public inbox for [email protected]  
help / color / mirror / Atom feed
From: Kirill Reshke <[email protected]>
To: Gurjeet Singh <[email protected]>
Cc: Jeff Davis <[email protected]>
Cc: Nathan Bossart <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Cc: Stephen Frost <[email protected]>
Cc: Brindle, Joshua <[email protected]>
Subject: Re: [PoC/RFC] Multiple passwords, interval expirations
Date: Wed, 10 Apr 2024 17:44:10 +0500
Message-ID: <CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com> (raw)
In-Reply-To: <CABwTF4V+veezGtNRiun1T_0u9kitS2Wk0VovKpUnZXdTCHvJ4w@mail.gmail.com>
References: <CAGB+Vh5Ed65gCmjJxCYguVoBoiDLePYDAeHMkb6-7PR-DFU-yQ@mail.gmail.com>
	<[email protected]>
	<CAOuzzgpyFRFbskZW0uYwP4_r5NkFJywTurn0SQ+pkbcuuQchjw@mail.gmail.com>
	<CABwTF4Uih+xKc8o7W-Qmzo2Oc3BbvnY+JHzcZ-HG2vDzBQpf8w@mail.gmail.com>
	<[email protected]>
	<[email protected]>
	<CAOuzzgq2j43X6vwF8LCPcPuKZ2gw0ZbF=CrsB6CmtEt1h2ci0g@mail.gmail.com>
	<CABwTF4XZO_429=QzZHt-1Q=KSCR2XjYG+xVy2p-PdPtun9tVdg@mail.gmail.com>
	<[email protected]>
	<CABwTF4Xf5qJgeapUvT3fbU+Er9sVKBEihcj7pb9_A4c5u1NaZA@mail.gmail.com>
	<20231005190453.GB151257@nathanxps13>
	<[email protected]>
	<CABwTF4Xm97B3d2OkVWWpS1GBrRY_zJY8bkmQzmzYnXM0NU3w5Q@mail.gmail.com>
	<[email protected]>
	<CABwTF4VLNXvMLYHvySmGJOBRuO1h6L7Wq4JWnKd7F2etJU5eXA@mail.gmail.com>
	<CABwTF4VvnkRUEKBT+j9fj9EyfGSFG3Ss3QejqfLy33DzKuc3DQ@mail.gmail.com>
	<CABwTF4VVYPja=f-jR8tRuNUo0Sr8eHSYQQWSBC3g7j3K2aYSbQ@mail.gmail.com>
	<CABwTF4V+veezGtNRiun1T_0u9kitS2Wk0VovKpUnZXdTCHvJ4w@mail.gmail.com>

Hi!

I'm interested in this feature presence in the PostgreSQL core. Will
try to provide valuable review/comments/suggestions and other help.

On Tue, 10 Oct 2023 at 16:17, Gurjeet Singh <[email protected]> wrote:
>
> > On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <[email protected]> wrote:
> > >
> > > Next steps:
> > > - Break the patch into a series of smaller patches.
> > > - Add TAP tests (test the ability to actually login with these passwords)
> > > - Add/update documentation
> > > - Add more regression tests
>
> Please see attached the v4 of the patchset that introduces the notion
> of named passwords slots, namely 'first' and 'second' passwords, and
> allows users to address each of these passwords separately for the
> purposes of adding, dropping, or assigning expiration times.
>
> Apart from the changes described by each patch's commit title, one
> significant change since v3 is that now (included in v4-0002...patch)
> it is not allowed for a role to have a mix of a types of passwords.
> When adding a password, the patch ensures that the password being
> added uses the same hashing algorithm (md5 or scram-sha-256) as the
> existing password, if any.  Having all passwords of the same type
> helps the server pick the corresponding authentication method during
> connection attempt.
>
> The v3 patch also had a few bugs that were exposed by cfbot's
> automatic run. All those bugs have now been fixed, and the latest run
> on the v4 branch [1] on my private Git repo shows a clean run [1].
>
> The list of patches, and their commit titles are as follows:
>
> > v4-0001-...patch Add new columns to pg_authid
> > v4-0002-...patch Update password verification infrastructure to handle two passwords
> > v4-0003-...patch Added SQL support for ALTER ROLE to manage two passwords
> > v4-0004-...patch Updated pg_dumpall to support exporting a role's second password
> > v4-0005-...patch Update system views pg_roles and pg_shadow
> > v4-0006-...patch Updated pg_authid catalog documentation
> > v4-0007-...patch Updated psql's describe-roles meta-command
> > v4-0008-...patch Added documentation for ALTER ROLE command
> > v4-0009-...patch Added TAP tests to prove that a role can use two passwords to login
> > v4-0010-...patch pgindent run
> > v4-0011-...patch Run pgperltidy on files changed by this patchset
>
> Running pgperltidy updated many perl files unrelated to this patch, so
> in the last patch I chose to include only the one perl file that is
> affected by this patchset.
>
> [1]: password_rollover_v4 (910f81be54)
> https://github.com/gurjeet/postgres/commits/password_rollover_v4
>
> [2]: https://cirrus-ci.com/build/4675613999497216
>
> Best regards,
> Gurjeet
> http://Gurje.et


Latest attachment does not apply to HEAD anymore.  I have rebased
them. While rebasing, a couple of minor changes were done:

1) Little correction in the `plain_crypt_verify` comment. IMO this
sounds a little better and comprehensible, is it?

> - * 'shadow_pass' is the user's correct password hash, as stored in
> - * pg_authid's rolpassword or rolsecondpassword.
> + * 'shadow_pass' is one of the user's correct password hashes, as stored in
> + * pg_authid's.

2) in v4-0004:

>        /* note: rolconfig is dumped later */
> -       if (server_version >= 90600)
> +       if (server_version >= 170000)
>                printfPQExpBuffer(buf,
>                                                  "SELECT oid, rolname, rolsuper, rolinherit, "
>                                                  "rolcreaterole, rolcreatedb, "
> -                                                 "rolcanlogin, rolconnlimit, rolpassword, "
> -                                                 "rolvaliduntil, rolreplication, rolbypassrls, "
> +                                                 "rolcanlogin, rolconnlimit, "
> +                                                 "rolpassword, rolvaliduntil, "
> +                                                 "rolsecondpassword, rolsecondvaliduntil, "
> +                                                 "rolreplication, rolbypassrls, "
> +                                                 "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
> +                                                 "rolname = current_user AS is_current_user "
> +                                                 "FROM %s "
> +                                                 "WHERE rolname !~ '^pg_' "
> +                                                 "ORDER BY 2", role_catalog, role_catalog);
> +       else if (server_version >= 90600)
> +               printfPQExpBuffer(buf,
> +                                                 "SELECT oid, rolname, rolsuper, rolinherit, "
> +                                                 "rolcreaterole, rolcreatedb, "
> +                                                 "rolcanlogin, rolconnlimit, "
> +                                                 "rolpassword, rolvaliduntil, "
> +                                                 "rolsecondpassword, rolsecodnvaliduntil, "
> +                                                 "null as rolsecondpassword, null as rolsecondvaliduntil, "
> +                                                 "rolreplication, rolbypassrls, "

Is it a bug in server_version < 17000 && server_version >= 90600 case?
we are trying to get   "rolsecondpassword, rolsecodnvaliduntil, " in
this case? I have removed this, let me know if there is my
misunderstanding.
Also, if stmt changed to ` if (server_version >= 180000)` since pg17
feature freeze.

v4-0005 - v4-000 Applied cleanly, didn't touch them. But, I haven't
reviewed them yet either.

v4-0010-pgindent-run.patch - is it actually needed?

Overall comments:

1) AFAIU we are forcing all passwords to have/interact with the same
salt. We really have no choice here, because in the case of SCRAM auth
salt is used client-side to authenticate the server.I dont feel this
is the best approach to restrict auth. process this much, though I
didn't come up with strong objections to it. Anyway, what if we give
the server a hint, which password to use in the startup message (in
case users have more than one password)? This way we still can use
different salts.

2) I'm also not a big fan of max-2-password restriction. There are
objections in the thread that having multiple passwords and named
passwords leads to problems on the server-side, but I think that at
least named passwords are a useful feature for those who use external
Vault systems for password management. In this case, you can set the
name of your password to the version of Vault secret, which is a very
natural thing to do (and, thus, support it).

3) Should we unite 0001 & 0004 patches in one? Or maybe 0003 & 0004?
It is not a good idea to commit one of these patches without
another.... (Since we can reach a database state that cannot be
`pg_dump`-ed)


So, that's it.


Attachments:

  [application/octet-stream] v5-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patch (36.8K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/2-v5-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patch)
  download | inline diff:
From bc598e00a3d57ad1a1d6e56fc7b969b0ad2886d6 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 11:54:11 -0700
Subject: [PATCH v5 3/9] Added SQL support for ALTER ROLE to manage two
 passwords

Disallow roles to have different types of passwords; when setting or
adding a password, ensure that it is of the same type as the type of the
other existing password, if any.
---
 src/backend/commands/user.c                   | 348 +++++++++++++++++-
 src/backend/parser/gram.y                     |  53 ++-
 .../regress/expected/password_rollover.out    | 161 ++++++++
 src/test/regress/parallel_schedule            |   5 +
 src/test/regress/sql/password_rollover.sql    | 130 +++++++
 5 files changed, 684 insertions(+), 13 deletions(-)
 create mode 100644 src/test/regress/expected/password_rollover.out
 create mode 100644 src/test/regress/sql/password_rollover.sql

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 54f9b3fb42..20090df58a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -720,11 +720,16 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char	   *second_password = NULL;	/* user's second password */
 	int			connlimit = -1; /* maximum connections allowed */
-	char	   *validUntil = NULL;	/* time the login is valid until */
-	Datum		validUntil_datum;	/* same, as timestamptz Datum */
+	char	   *validUntil = NULL;	/* time the password is valid until */
+	Datum		validUntil_datum;	/* validUntil, as timestamptz Datum */
 	bool		validUntil_null;
+	char	   *secondValidUntil = NULL;/* time the second password is valid until */
+	Datum		secondValidUntil_datum;	/* secondValidUntil, as timestamptz Datum */
+	bool		secondValidUntil_null;
 	DefElem    *dpassword = NULL;
+	DefElem    *dsecondpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
 	DefElem    *dcreaterole = NULL;
@@ -734,10 +739,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *dconnlimit = NULL;
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
+	DefElem    *dfirstValidUntil = NULL;
+	DefElem    *dsecondValidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	Oid			roleid;
 	Oid			currentUserId = GetUserId();
 	GrantRoleOptions popt;
+	bool		overwriteFirstPassword = false;
+	bool		addFirstPassword = false;
+	bool		addSecondPassword = false;
+	bool		dropFirstPassword = false;
+	bool		dropSecondPassword = false;
+	bool		dropAllPasswords = false;
 
 	check_rolespec_name(stmt->role,
 						_("Cannot alter reserved roles."));
@@ -749,9 +762,95 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		if (strcmp(defel->defname, "password") == 0)
 		{
-			if (dpassword)
+			if (overwriteFirstPassword || addFirstPassword)
+				errorConflictingDefElem(defel, pstate);
+			dpassword = defel;
+			overwriteFirstPassword = true;
+
+			if (dpassword->arg != NULL)
+			{
+				/* PASSWORD 'sometext' syntax was used */
+
+				/*
+				 * Adding and dropping passwords in the same command is not
+				 * supported.
+				 */
+				if (dropFirstPassword || dropSecondPassword || dropAllPasswords)
+					errorConflictingDefElem(defel, pstate);
+			}
+			else
+			{
+				/* PASSWORD NULL syntax was used */
+
+				if (dropFirstPassword)
+					errorConflictingDefElem(defel, pstate);
+
+				/*
+				 * Adding and dropping passwords in the same command is not
+				 * supported.
+				 */
+				if (addFirstPassword || addSecondPassword)
+					errorConflictingDefElem(defel, pstate);
+
+				dropFirstPassword = true;
+			}
+		}
+		else if (strcmp(defel->defname, "add-first-password") == 0)
+		{
+			if (addFirstPassword || overwriteFirstPassword)
 				errorConflictingDefElem(defel, pstate);
 			dpassword = defel;
+			addFirstPassword = true;
+
+			/*
+			 * Adding and dropping passwords in the same command is not
+			 * supported.
+			 */
+			if (dropFirstPassword || dropSecondPassword || dropAllPasswords)
+				errorConflictingDefElem(defel, pstate);
+		}
+		else if (strcmp(defel->defname, "add-second-password") == 0)
+		{
+			if (dsecondpassword)
+				errorConflictingDefElem(defel, pstate);
+			dsecondpassword = defel;
+			addSecondPassword = true;
+			/*
+			 * Adding and dropping passwords in the same command is not
+			 * supported.
+			 */
+			if (dropFirstPassword || dropSecondPassword || dropAllPasswords)
+				errorConflictingDefElem(defel, pstate);
+		}
+		else if (strcmp(defel->defname, "drop-password") == 0)
+		{
+			char *which = strVal(defel->arg);
+
+			if (strcmp(which, "first") == 0)
+			{
+				if (dropFirstPassword || dropAllPasswords)
+					errorConflictingDefElem(defel, pstate);
+				dropFirstPassword = true;
+			}
+			else if (strcmp(which, "second") == 0)
+			{
+				if (dropSecondPassword || dropAllPasswords)
+					errorConflictingDefElem(defel, pstate);
+				dropSecondPassword = true;
+			}
+			else
+			{
+				if (dropAllPasswords || dropFirstPassword || dropSecondPassword)
+					errorConflictingDefElem(defel, pstate);
+				dropAllPasswords = true;
+			}
+
+			/*
+			 * Adding and dropping passwords in the same command is not
+			 * supported.
+			 */
+			if (addFirstPassword || addSecondPassword || overwriteFirstPassword)
+				errorConflictingDefElem(defel, pstate);
 		}
 		else if (strcmp(defel->defname, "superuser") == 0)
 		{
@@ -808,6 +907,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dvalidUntil = defel;
 		}
+		else if (strcmp(defel->defname, "first-password-valid-until") == 0)
+		{
+			if (dfirstValidUntil)
+				errorConflictingDefElem(defel, pstate);
+			dfirstValidUntil = defel;
+		}
+		else if (strcmp(defel->defname, "second-password-valid-until") == 0)
+		{
+			if (dsecondValidUntil)
+				errorConflictingDefElem(defel, pstate);
+			dsecondValidUntil = defel;
+		}
 		else if (strcmp(defel->defname, "bypassrls") == 0)
 		{
 			if (dbypassRLS)
@@ -821,6 +932,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 	if (dpassword && dpassword->arg)
 		password = strVal(dpassword->arg);
+	if (dsecondpassword)
+		second_password = strVal(dsecondpassword->arg);
 	if (dconnlimit)
 	{
 		connlimit = intVal(dconnlimit->arg);
@@ -829,8 +942,30 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("invalid connection limit: %d", connlimit)));
 	}
+
+	/*
+	 * Disallow mixing VALID UNTIL with ADD FIRST/SECOND PASSWORD.
+	 *
+	 * VALID UNTIL and FIRST PASSWORD VALID UNTIL are functionally identical,
+	 * but we track them separately to prevent the confusing invocation like the
+	 * following.
+	 *
+	 * ALTER ROLE x ADD SECOND PASSWORD 'y' VALID UNTIL '2020/01/01';
+	 *
+	 * In the above command the user may expect the expiration of the _second_
+	 * password to be set to '2020/01/01', but it will lead to second password's
+	 * expiration set to NULL and first password's expiration set to
+	 * '2020/01/01', because a plain VALIF UNTIL applies to the _first_
+	 * password.
+	 */
+	if (dvalidUntil && (addFirstPassword || addSecondPassword))
+		errorConflictingDefElem(dvalidUntil, pstate);
+	dvalidUntil = dfirstValidUntil;
+
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dsecondValidUntil)
+		secondValidUntil = strVal(dsecondValidUntil->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -866,7 +1001,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	{
 		/* things an unprivileged user certainly can't do */
 		if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
-			dvalidUntil || disreplication || dbypassRLS)
+			dvalidUntil || dsecondValidUntil || disreplication || dbypassRLS)
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to alter role"),
@@ -874,7 +1009,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 							   "CREATEROLE", "ADMIN", rolename)));
 
 		/* an unprivileged user can change their own password */
-		if (dpassword && roleid != currentUserId)
+		if ((dpassword || dsecondpassword) && roleid != currentUserId)
 			ereport(ERROR,
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to alter role"),
@@ -933,15 +1068,42 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 										   &validUntil_null);
 	}
 
+	/* Convert secondvaliduntil to internal form */
+	if (dsecondValidUntil)
+	{
+		secondValidUntil_datum = DirectFunctionCall3(timestamptz_in,
+											   CStringGetDatum(secondValidUntil),
+											   ObjectIdGetDatum(InvalidOid),
+											   Int32GetDatum(-1));
+		secondValidUntil_null = false;
+	}
+	else
+	{
+		/* fetch existing setting in case hook needs it */
+		secondValidUntil_datum = SysCacheGetAttr(AUTHNAME, tuple,
+										   Anum_pg_authid_rolsecondvaliduntil,
+										   &secondValidUntil_null);
+	}
+
 	/*
 	 * Call the password checking hook if there is one defined
 	 */
-	if (check_password_hook && password)
-		(*check_password_hook) (rolename,
-								password,
-								get_password_type(password),
-								validUntil_datum,
-								validUntil_null);
+	if (check_password_hook)
+	{
+		if (password)
+			(*check_password_hook) (rolename,
+									password,
+									get_password_type(password),
+									validUntil_datum,
+									validUntil_null);
+
+		if (second_password)
+			(*check_password_hook) (rolename,
+									second_password,
+									get_password_type(second_password),
+									secondValidUntil_datum,
+									secondValidUntil_null);
+	}
 
 	/*
 	 * Build an updated tuple, perusing the information just obtained
@@ -1007,6 +1169,20 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		char	   *shadow_pass;
 		const char *logdetail = NULL;
 
+		if (addFirstPassword)
+		{
+			bool	firstPassword_null;
+
+			SysCacheGetAttr(AUTHNAME, tuple,
+							Anum_pg_authid_rolpassword,
+							&firstPassword_null);
+
+			if (!firstPassword_null)
+				ereport(ERROR,
+						(errmsg("first password is already in use"),
+						errdetail("Use ALTER ROLE DROP FIRST PASSWORD.")));
+		}
+
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (password[0] == '\0' ||
 			plain_crypt_verify(rolename, password, "", &logdetail) == STATUS_OK)
@@ -1033,17 +1209,153 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
 	}
 
+	/* second password */
+	if (second_password)
+	{
+		char	   *shadow_pass;
+		const char *logdetail = NULL;
+		bool		secondPassword_null;
+
+		SysCacheGetAttr(AUTHNAME, tuple,
+						Anum_pg_authid_rolsecondpassword,
+						&secondPassword_null);
+
+		if (!secondPassword_null)
+			ereport(ERROR,
+					(errmsg("second password is already in use"),
+					errdetail("Use ALTER ROLE DROP SECOND PASSWORD")));
+
+		/* Like in CREATE USER, don't allow an empty password. */
+		if (second_password[0] == '\0' ||
+			plain_crypt_verify(rolename, second_password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+			new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] = true;
+		}
+		else
+		{
+			char	   *salt;
+
+			if (!get_salt(rolename, &salt, &logdetail))
+				ereport(ERROR,
+						(errcode(ERRCODE_INTERNAL_ERROR),
+						errmsg("could not get a valid salt for password"),
+						errdetail("%s", logdetail)));
+
+			/* Encrypt the password to the requested format. */
+			shadow_pass = encrypt_password(Password_encryption, salt, second_password);
+			new_record[Anum_pg_authid_rolsecondpassword - 1] =
+				CStringGetTextDatum(shadow_pass);
+		}
+		new_record_repl[Anum_pg_authid_rolsecondpassword - 1] = true;
+	}
+
+	/*
+	 * Disallow more than one type of passwords for a role. If a role has an md5
+	 * password, then allow only md5 passwords; similarly for scram-sha-256
+	 * passwords. Having all passwords of the same type helps the server pick
+	 * the correponding authentication method during connection attempt.
+	 */
+	if (overwriteFirstPassword || addFirstPassword || addSecondPassword)
+	{
+		bool	firstPassword_null;
+		bool	secondPassword_null;
+		Datum	firstPassword_datum;
+		Datum	secondPassword_datum;
+		char   *roleFirstPassword = NULL;
+		char   *roleSecondPassword = NULL;
+		PasswordType roleFirstPasswordType = -1; /* silence the compiler */
+		PasswordType roleSecondPasswordType = -1; /* silence the compiler */
+
+		firstPassword_datum = SysCacheGetAttr(AUTHNAME, tuple,
+												Anum_pg_authid_rolpassword,
+												&firstPassword_null);
+		secondPassword_datum = SysCacheGetAttr(AUTHNAME, tuple,
+												Anum_pg_authid_rolsecondpassword,
+												&secondPassword_null);
+		if (!firstPassword_null)
+		{
+			roleFirstPassword = TextDatumGetCString(firstPassword_datum);
+			roleFirstPasswordType = get_password_type(roleFirstPassword);
+		}
+
+		if (!secondPassword_null)
+		{
+			roleSecondPassword = TextDatumGetCString(secondPassword_datum);
+			roleSecondPasswordType = get_password_type(roleSecondPassword);
+		}
+
+			/* if the user requested setting the first password ... */
+		if ((overwriteFirstPassword || addFirstPassword) &&
+			/* and we have decided to honor their request */
+			new_record_repl[Anum_pg_authid_rolpassword - 1] == true &&
+			/* and the resulting password hash about to be stored is not null */
+			new_record_nulls[Anum_pg_authid_rolpassword - 1] == false &&
+			/* and the algorithm used doesn't match existing password's algorithm */
+			roleSecondPassword != NULL &&
+			roleSecondPasswordType != Password_encryption)
+		{
+			if (roleSecondPasswordType == PASSWORD_TYPE_MD5)
+				ereport(ERROR,
+						(errmsg("role has an md5 password"),
+						errdetail("The new password must also use md5.")));
+			else if (roleSecondPasswordType == PASSWORD_TYPE_SCRAM_SHA_256)
+				ereport(ERROR,
+						(errmsg("role has a scram-sha-256 password"),
+						errdetail("The new password must also use scram-sha-256.")));
+			else
+				ereport(ERROR,
+						(errmsg("role has a plaintext password"),
+						errdetail("The new password must also use plaintext.")));
+		}
+
+			/* if the user requested setting the second password ... */
+		if (addSecondPassword &&
+			/* and we have decided to honor their request */
+			new_record_repl[Anum_pg_authid_rolsecondpassword - 1] == true &&
+			/* and the resulting password hash about to be stored is not null */
+			new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] == false &&
+			/* and the algorithm used doesn't match existing password's algorithm */
+			roleFirstPassword != NULL &&
+			roleFirstPasswordType != Password_encryption)
+		{
+			if (roleFirstPasswordType == PASSWORD_TYPE_MD5)
+				ereport(ERROR,
+						(errmsg("role has an md5 password"),
+						errdetail("The new password must also use md5.")));
+			else if (roleFirstPasswordType == PASSWORD_TYPE_SCRAM_SHA_256)
+				ereport(ERROR,
+						(errmsg("role has a scram-sha-256 password"),
+						errdetail("The new password must also use scram-sha-256.")));
+			else
+				ereport(ERROR,
+						(errmsg("role has a plaintext password"),
+						errdetail("The new password must also use plaintext.")));
+		}
+	}
+
 	/* unset password */
-	if (dpassword && dpassword->arg == NULL)
+	if (dropFirstPassword || dropAllPasswords)
 	{
 		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
 		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
 	}
 
+	if (dropSecondPassword || dropAllPasswords)
+	{
+		new_record_repl[Anum_pg_authid_rolsecondpassword - 1] = true;
+		new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] = true;
+	}
+
 	/* valid until */
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 	new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true;
+	/* second password valid until */
+	new_record[Anum_pg_authid_rolsecondvaliduntil - 1] = secondValidUntil_datum;
+	new_record_nulls[Anum_pg_authid_rolsecondvaliduntil - 1] = secondValidUntil_null;
+	new_record_repl[Anum_pg_authid_rolsecondvaliduntil - 1] = true;
 
 	if (dbypassRLS)
 	{
@@ -1552,6 +1864,18 @@ RenameRole(const char *oldname, const char *newname)
 				(errmsg("MD5 password cleared because of role rename")));
 	}
 
+	datum = heap_getattr(oldtuple, Anum_pg_authid_rolsecondpassword, dsc, &isnull);
+
+	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+	{
+		/* MD5 uses the username as salt, so just clear it on a rename */
+		repl_repl[Anum_pg_authid_rolsecondpassword - 1] = true;
+		repl_null[Anum_pg_authid_rolsecondpassword - 1] = true;
+
+		ereport(NOTICE,
+				(errmsg("MD5 password cleared because of role rename")));
+	}
+
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0523f7e891..d3930b28fb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -364,7 +364,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	opt_nowait_or_skip
 
 %type <list>	OptRoleList AlterOptRoleList
-%type <defelt>	CreateOptRoleElem AlterOptRoleElem
+%type <defelt>	CreateOptRoleElem AlterOptRoleElem AlterOnlyOptRoleElem
+%type <boolean>	OptFirstOrSecond
 
 %type <str>		opt_type
 %type <str>		foreign_server_version opt_foreign_server_version
@@ -1206,6 +1207,7 @@ OptRoleList:
 
 AlterOptRoleList:
 			AlterOptRoleList AlterOptRoleElem		{ $$ = lappend($1, $2); }
+			| AlterOptRoleList AlterOnlyOptRoleElem	{ $$ = lappend($1, $2); }
 			| /* EMPTY */							{ $$ = NIL; }
 		;
 
@@ -1301,6 +1303,55 @@ AlterOptRoleElem:
 				}
 		;
 
+OptFirstOrSecond:
+			FIRST_P 			{ $$ = true; }
+			| SECOND_P 			{ $$ = false; }
+		;
+
+/*
+ * AlterOnlyOptRoleElem is separate from AlterOptRoleElem because these options
+ * are not available to the CREATE ROLE command.
+ */
+AlterOnlyOptRoleElem:
+			ADD_P OptFirstOrSecond PASSWORD Sconst
+				{
+					bool first = $2;
+
+					if (first)
+						$$ = makeDefElem("add-first-password",
+										(Node *) makeString($4), @1);
+					else
+						$$ = makeDefElem("add-second-password",
+										(Node *) makeString($4), @1);
+				}
+			| DROP OptFirstOrSecond PASSWORD
+				{
+					bool first = $2;
+
+					if (first)
+						$$ = makeDefElem("drop-password",
+										(Node *) makeString("first"), @1);
+					else
+						$$ = makeDefElem("drop-password",
+										(Node *) makeString("second"), @1);
+				}
+			| DROP ALL PASSWORD
+				{
+					$$ = makeDefElem("drop-all-password", (Node *) NULL, @1);
+				}
+			| OptFirstOrSecond PASSWORD VALID UNTIL Sconst
+				{
+					bool first = $1;
+
+					if (first)
+						$$ = makeDefElem("first-password-valid-until",
+										(Node *) makeString($5), @1);
+					else
+						$$ = makeDefElem("second-password-valid-until",
+										(Node *) makeString($5), @1);
+				}
+		;
+
 CreateOptRoleElem:
 			AlterOptRoleElem			{ $$ = $1; }
 			/* The following are not supported by ALTER ROLE/USER/GROUP */
diff --git a/src/test/regress/expected/password_rollover.out b/src/test/regress/expected/password_rollover.out
new file mode 100644
index 0000000000..44522987f8
--- /dev/null
+++ b/src/test/regress/expected/password_rollover.out
@@ -0,0 +1,161 @@
+--
+-- Tests for password rollovers
+--
+SET password_encryption = 'md5';
+-- Create a role, as usual
+CREATE ROLE regress_password_rollover1 PASSWORD 'p1' LOGIN;
+-- the rolpassword field should be non-null, and others should be null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |             rolpassword             | rolvaliduntil | rolsecondpassword | rolsecondvaliduntil 
+----------------------------+-------------------------------------+---------------+-------------------+---------------------
+ regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 |               |                   | 
+(1 row)
+
+-- Add another password that the role can use for authentication.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p2';
+-- the rolpassword and rolsecondpassword fields should be non-null, and others should be null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |             rolpassword             | rolvaliduntil |          rolsecondpassword          | rolsecondvaliduntil 
+----------------------------+-------------------------------------+---------------+-------------------------------------+---------------------
+ regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 |               | md5c72e860974ea678511e200ded12780b6 | 
+(1 row)
+
+-- Set second password's expiration time.
+ALTER ROLE regress_password_rollover1 SECOND PASSWORD VALID UNTIL '2021/01/01';
+-- the rolvaliduntil field should be null, and other should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |             rolpassword             | rolvaliduntil |          rolsecondpassword          |     rolsecondvaliduntil      
+----------------------------+-------------------------------------+---------------+-------------------------------------+------------------------------
+ regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 |               | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST
+(1 row)
+
+ALTER ROLE regress_password_rollover1 FIRST PASSWORD VALID UNTIL '2022/01/01';
+-- All fields should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |             rolpassword             |        rolvaliduntil         |          rolsecondpassword          |     rolsecondvaliduntil      
+----------------------------+-------------------------------------+------------------------------+-------------------------------------+------------------------------
+ regress_password_rollover1 | md54ec11153dc2e0022e0d556740a238e94 | Sat Jan 01 00:00:00 2022 PST | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST
+(1 row)
+
+-- Setting a password to null does not set its expiration time to null
+ALTER ROLE regress_password_rollover1 PASSWORD NULL;
+-- the rolpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           | rolpassword |        rolvaliduntil         |          rolsecondpassword          |     rolsecondvaliduntil      
+----------------------------+-------------+------------------------------+-------------------------------------+------------------------------
+ regress_password_rollover1 |             | Sat Jan 01 00:00:00 2022 PST | md5c72e860974ea678511e200ded12780b6 | Fri Jan 01 00:00:00 2021 PST
+(1 row)
+
+-- If, for some reason, the role wants to get rid of the latest password added.
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+-- the rolpassword and rolsecondpassword fields should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           | rolpassword |        rolvaliduntil         | rolsecondpassword |     rolsecondvaliduntil      
+----------------------------+-------------+------------------------------+-------------------+------------------------------
+ regress_password_rollover1 |             | Sat Jan 01 00:00:00 2022 PST |                   | Fri Jan 01 00:00:00 2021 PST
+(1 row)
+
+-- Add a new password in 'second' slot
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p3' SECOND PASSWORD VALID UNTIL '2023/01/01';
+-- the rolpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           | rolpassword |        rolvaliduntil         |          rolsecondpassword          |     rolsecondvaliduntil      
+----------------------------+-------------+------------------------------+-------------------------------------+------------------------------
+ regress_password_rollover1 |             | Sat Jan 01 00:00:00 2022 PST | md53dff5d9eee2beb63399f1900a2371fcb | Sun Jan 01 00:00:00 2023 PST
+(1 row)
+
+-- VALID UNTIL must not be allowed when ADDing a password, to avoid the
+-- confusing invocation where the command may seem to do one thing but actually
+-- does something else. The following may seem like it will add a 'second'
+-- password with a new expiration, but, if allowed, this will set the expiration
+-- time on the _first_ password.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTIL '2023/01/01';
+ERROR:  conflicting or redundant options
+LINE 1: ...gress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTI...
+                                                             ^
+-- Even though both, the password and the expiration, refer to the first
+-- password, we disallow it to be consistent with the previous command's
+-- behaviour.
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTIL '2023/01/01';
+ERROR:  conflicting or redundant options
+LINE 1: ...egress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTI...
+                                                             ^
+-- Set the first password
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p5';
+-- Attempting to add a password while the respective slot is occupied
+-- results in error
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p6';
+ERROR:  first password is already in use
+DETAIL:  Use ALTER ROLE DROP FIRST PASSWORD.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p6';
+ERROR:  second password is already in use
+DETAIL:  Use ALTER ROLE DROP SECOND PASSWORD
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+-- The rolsecondpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |             rolpassword             |        rolvaliduntil         | rolsecondpassword |     rolsecondvaliduntil      
+----------------------------+-------------------------------------+------------------------------+-------------------+------------------------------
+ regress_password_rollover1 | md5cc8c5dac5560a2fead71cfba4625a2c7 | Sat Jan 01 00:00:00 2022 PST |                   | Sun Jan 01 00:00:00 2023 PST
+(1 row)
+
+-- Use scram-sha-256 for password storage
+SET password_encryption = 'scram-sha-256';
+-- Trying to add a scram-sha-256 based password, while the other password uses
+-- md5, should raise an error.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+ERROR:  role has an md5 password
+DETAIL:  The new password must also use md5.
+-- Drop the first password
+ALTER ROLE regress_password_rollover1 DROP FIRST PASSWORD;
+-- Adding a scram-sha-256 based password should now be allowed.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+-- The rolsecondpassword field should now contain a SCRAM secret, and the rolpassword field should now be null.
+SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolsecondpassword_masked, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           | rolpassword |        rolvaliduntil         |             rolsecondpassword_masked              | rolsecondvaliduntil 
+----------------------------+-------------+------------------------------+---------------------------------------------------+---------------------
+ regress_password_rollover1 |             | Sat Jan 01 00:00:00 2022 PST | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> | infinity
+(1 row)
+
+-- Adding another scram-sha-256 based password should also be allowed.
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p8'
+    FIRST PASSWORD VALID UNTIL 'Infinity';
+-- The rolpassword and rolsecondpassword field should now contain a SCRAM secret
+SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolsecondpassword_masked, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+          rolname           |                rolpassword_masked                 | rolvaliduntil |             rolsecondpassword_masked              | rolsecondvaliduntil 
+----------------------------+---------------------------------------------------+---------------+---------------------------------------------------+---------------------
+ regress_password_rollover1 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> | infinity      | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> | infinity
+(1 row)
+
+-- Switch back to md5 for password storage
+SET password_encryption = 'md5';
+-- Ensure the second password is empty, before we populate it
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+-- Trying to add an md5 based password, while the other password uses
+-- scram-sha-256, should raise an error.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p9'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+ERROR:  role has a scram-sha-256 password
+DETAIL:  The new password must also use scram-sha-256.
+DROP ROLE regress_password_rollover1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c567617..9bb5933607 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,11 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated join_hash
 
+# ----------
+# Another group of parallel tests
+# ----------
+test: password_rollover
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/password_rollover.sql b/src/test/regress/sql/password_rollover.sql
new file mode 100644
index 0000000000..f630da30c2
--- /dev/null
+++ b/src/test/regress/sql/password_rollover.sql
@@ -0,0 +1,130 @@
+--
+-- Tests for password rollovers
+--
+
+SET password_encryption = 'md5';
+
+-- Create a role, as usual
+CREATE ROLE regress_password_rollover1 PASSWORD 'p1' LOGIN;
+
+-- the rolpassword field should be non-null, and others should be null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Add another password that the role can use for authentication.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p2';
+
+-- the rolpassword and rolsecondpassword fields should be non-null, and others should be null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Set second password's expiration time.
+ALTER ROLE regress_password_rollover1 SECOND PASSWORD VALID UNTIL '2021/01/01';
+
+-- the rolvaliduntil field should be null, and other should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+ALTER ROLE regress_password_rollover1 FIRST PASSWORD VALID UNTIL '2022/01/01';
+
+-- All fields should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Setting a password to null does not set its expiration time to null
+ALTER ROLE regress_password_rollover1 PASSWORD NULL;
+
+-- the rolpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- If, for some reason, the role wants to get rid of the latest password added.
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+
+-- the rolpassword and rolsecondpassword fields should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Add a new password in 'second' slot
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p3' SECOND PASSWORD VALID UNTIL '2023/01/01';
+
+-- the rolpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- VALID UNTIL must not be allowed when ADDing a password, to avoid the
+-- confusing invocation where the command may seem to do one thing but actually
+-- does something else. The following may seem like it will add a 'second'
+-- password with a new expiration, but, if allowed, this will set the expiration
+-- time on the _first_ password.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p4' VALID UNTIL '2023/01/01';
+
+-- Even though both, the password and the expiration, refer to the first
+-- password, we disallow it to be consistent with the previous command's
+-- behaviour.
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p4' VALID UNTIL '2023/01/01';
+
+-- Set the first password
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p5';
+
+-- Attempting to add a password while the respective slot is occupied
+-- results in error
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p6';
+
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p6';
+
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+
+-- The rolsecondpassword field should be null, and others should be non-null
+SELECT rolname, rolpassword, rolvaliduntil, rolsecondpassword, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Use scram-sha-256 for password storage
+SET password_encryption = 'scram-sha-256';
+
+-- Trying to add a scram-sha-256 based password, while the other password uses
+-- md5, should raise an error.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+
+-- Drop the first password
+ALTER ROLE regress_password_rollover1 DROP FIRST PASSWORD;
+
+-- Adding a scram-sha-256 based password should now be allowed.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+
+-- The rolsecondpassword field should now contain a SCRAM secret, and the rolpassword field should now be null.
+SELECT rolname, rolpassword, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolsecondpassword_masked, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Adding another scram-sha-256 based password should also be allowed.
+ALTER ROLE regress_password_rollover1 ADD FIRST PASSWORD 'p8'
+    FIRST PASSWORD VALID UNTIL 'Infinity';
+
+-- The rolpassword and rolsecondpassword field should now contain a SCRAM secret
+SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked, rolvaliduntil, regexp_replace(rolsecondpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolsecondpassword_masked, rolsecondvaliduntil
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_password_rollover%';
+
+-- Switch back to md5 for password storage
+SET password_encryption = 'md5';
+
+-- Ensure the second password is empty, before we populate it
+ALTER ROLE regress_password_rollover1 DROP SECOND PASSWORD;
+
+-- Trying to add an md5 based password, while the other password uses
+-- scram-sha-256, should raise an error.
+ALTER ROLE regress_password_rollover1 ADD SECOND PASSWORD 'p9'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+
+DROP ROLE regress_password_rollover1;
-- 
2.25.1



  [application/octet-stream] v5-0001-Add-new-columns-to-pg_authid.patch (9.1K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/3-v5-0001-Add-new-columns-to-pg_authid.patch)
  download | inline diff:
From b016e1dca81947d811425d2b43b050891bd80739 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 11:36:05 -0700
Subject: [PATCH v5 1/9] Add new columns to pg_authid

Add two columns to pg_authid, namely rolsecondpassword and
rolsecondvaliduntil. These columns are added in preparation for the
password-rollover feature. These columns will store the hash and the
expiration time, repspectively, of a second password that the role can
use for login authentication.
---
 src/backend/commands/user.c       |  4 +++
 src/include/catalog/pg_authid.dat | 48 ++++++++++++++++++++-----------
 src/include/catalog/pg_authid.h   |  2 ++
 3 files changed, 38 insertions(+), 16 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index c75cde2e8e..32711f413d 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -451,9 +451,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	else
 		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
 
+	new_record_nulls[Anum_pg_authid_rolsecondpassword - 1] = true;
+
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
+	new_record_nulls[Anum_pg_authid_rolsecondvaliduntil - 1] = true;
+
 	new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls);
 
 	/*
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 55cabdda6f..8da09d1df7 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,81 +23,97 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '6171', oid_symbol => 'ROLE_PG_DATABASE_OWNER',
   rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '6181', oid_symbol => 'ROLE_PG_READ_ALL_DATA',
   rolname => 'pg_read_all_data', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '6182', oid_symbol => 'ROLE_PG_WRITE_ALL_DATA',
   rolname => 'pg_write_all_data', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '3373', oid_symbol => 'ROLE_PG_MONITOR',
   rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '3374', oid_symbol => 'ROLE_PG_READ_ALL_SETTINGS',
   rolname => 'pg_read_all_settings', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '3375', oid_symbol => 'ROLE_PG_READ_ALL_STATS',
   rolname => 'pg_read_all_stats', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '3377', oid_symbol => 'ROLE_PG_STAT_SCAN_TABLES',
   rolname => 'pg_stat_scan_tables', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4569', oid_symbol => 'ROLE_PG_READ_SERVER_FILES',
   rolname => 'pg_read_server_files', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4570', oid_symbol => 'ROLE_PG_WRITE_SERVER_FILES',
   rolname => 'pg_write_server_files', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4571', oid_symbol => 'ROLE_PG_EXECUTE_SERVER_PROGRAM',
   rolname => 'pg_execute_server_program', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4200', oid_symbol => 'ROLE_PG_SIGNAL_BACKEND',
   rolname => 'pg_signal_backend', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINT',
   rolname => 'pg_checkpoint', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_' ,
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '9256', oid_symbol => 'ROLE_PG_MAINTAIN',
   rolname => 'pg_maintain', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_' ,
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '4550', oid_symbol => 'ROLE_PG_USE_RESERVED_CONNECTIONS',
   rolname => 'pg_use_reserved_connections', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 { oid => '6304', oid_symbol => 'ROLE_PG_CREATE_SUBSCRIPTION',
   rolname => 'pg_create_subscription', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolpassword => '_null_', rolvaliduntil => '_null_',
+  rolsecondpassword => '_null_', rolsecondvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index e08863f78a..5dca4a12b0 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -45,6 +45,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	text		rolpassword;	/* password, if any */
 	timestamptz rolvaliduntil;	/* password expiration time, if any */
+	text		rolsecondpassword;	/* second password, if any */
+	timestamptz rolsecondvaliduntil;	/* second password expiration time, if any */
 #endif
 } FormData_pg_authid;
 
-- 
2.25.1



  [application/octet-stream] v5-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patch (4.1K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/4-v5-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patch)
  download | inline diff:
From 4e3603b98c689d88670d6cf454cbe988aa0134cc Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 13:49:03 -0700
Subject: [PATCH v5 4/9] Updated pg_dumpall to support exporting a role's
 second password

---
 src/bin/pg_dump/pg_dumpall.c | 43 +++++++++++++++++++++++++++++++-----
 1 file changed, 37 insertions(+), 6 deletions(-)

diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..3d2c450649 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -761,6 +761,8 @@ dumpRoles(PGconn *conn)
 				i_rolconnlimit,
 				i_rolpassword,
 				i_rolvaliduntil,
+				i_rolsecondpassword,
+				i_rolsecondvaliduntil,
 				i_rolreplication,
 				i_rolbypassrls,
 				i_rolcomment,
@@ -771,7 +773,20 @@ dumpRoles(PGconn *conn)
 	 * Notes: rolconfig is dumped later, and pg_authid must be used for
 	 * extracting rolcomment regardless of role_catalog.
 	 */
-	if (server_version >= 90600)
+	if (server_version >= 180000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, rolpassword, "
+						  "rolvaliduntil, "
+						  "rolsecondpassword, rolsecondvaliduntil, "
+						  "rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
@@ -787,7 +802,9 @@ dumpRoles(PGconn *conn)
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
 						  "rolcanlogin, rolconnlimit, rolpassword, "
-						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "rolvaliduntil, "
+						  "null as rolsecondpassword, null as rolsecodnvaliduntil, "
+						  "rolreplication, rolbypassrls, "
 						  "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, "
 						  "rolname = current_user AS is_current_user "
 						  "FROM %s "
@@ -796,8 +813,10 @@ dumpRoles(PGconn *conn)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
-						  "rolcanlogin, rolconnlimit, rolpassword, "
-						  "rolvaliduntil, rolreplication, "
+						  "rolcanlogin, rolconnlimit, "
+						  "rolpassword, rolvaliduntil, "
+						  "null as rolsecondpassword, null as rolsecondvaliduntil, "
+						  "rolreplication, "
 						  "false as rolbypassrls, "
 						  "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, "
 						  "rolname = current_user AS is_current_user "
@@ -816,6 +835,8 @@ dumpRoles(PGconn *conn)
 	i_rolconnlimit = PQfnumber(res, "rolconnlimit");
 	i_rolpassword = PQfnumber(res, "rolpassword");
 	i_rolvaliduntil = PQfnumber(res, "rolvaliduntil");
+	i_rolsecondpassword = PQfnumber(res, "rolsecondpassword");
+	i_rolsecondvaliduntil = PQfnumber(res, "rolsecondvaliduntil");
 	i_rolreplication = PQfnumber(res, "rolreplication");
 	i_rolbypassrls = PQfnumber(res, "rolbypassrls");
 	i_rolcomment = PQfnumber(res, "rolcomment");
@@ -904,12 +925,22 @@ dumpRoles(PGconn *conn)
 
 		if (!PQgetisnull(res, i, i_rolpassword) && !no_role_passwords)
 		{
-			appendPQExpBufferStr(buf, " PASSWORD ");
+			appendPQExpBufferStr(buf, " ADD FIRST PASSWORD ");
 			appendStringLiteralConn(buf, PQgetvalue(res, i, i_rolpassword), conn);
 		}
 
 		if (!PQgetisnull(res, i, i_rolvaliduntil))
-			appendPQExpBuffer(buf, " VALID UNTIL '%s'",
+			appendPQExpBuffer(buf, " FIRST PASSWORD VALID UNTIL '%s'",
+							  PQgetvalue(res, i, i_rolvaliduntil));
+
+		if (!PQgetisnull(res, i, i_rolsecondpassword) && !no_role_passwords)
+		{
+			appendPQExpBufferStr(buf, " ADD SECOND PASSWORD ");
+			appendStringLiteralConn(buf, PQgetvalue(res, i, i_rolsecondpassword), conn);
+		}
+
+		if (!PQgetisnull(res, i, i_rolsecondvaliduntil))
+			appendPQExpBuffer(buf, " SECOND PASSWORD VALID UNTIL '%s'",
 							  PQgetvalue(res, i, i_rolvaliduntil));
 
 		appendPQExpBufferStr(buf, ";\n");
-- 
2.25.1



  [application/octet-stream] v5-0002-Update-password-verification-infrastructure-to-ha.patch (34.6K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/5-v5-0002-Update-password-verification-infrastructure-to-ha.patch)
  download | inline diff:
From d62c9741d71f46a672d5675e0dbbe8d956d79242 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 11:48:11 -0700
Subject: [PATCH v5 2/9] Update password verification infrastructure to handle
 two passwords

After the addition of rolsecondpassword and rolsecondvaliduntil columns
to pg_authid, this commit adds the ability to honor the second
password as well, if any, to authenticate the roles.

The get_role_passwords() function retrieves and returns all valid
passwords for a role. It does so by inspecting rolpassword and
rolsecondpassword column values, and their respective rol*validuntil
column values.

get_salt(), a local function in user.c helps to extract the salt, needed
for generating hash of new passwords, from currently stored password
hashes, if any. For md5 it simply uses the role name, and for
scram-sha-256 it extracts and returns the salt from the stored hash.

The salt provided by get_salt() is used by CreateRole() and AlterRole()
to hash the new passwords of a role.

The following functions used to accept and peruse just one password /
secret. They are now updated to accept and use two passwords, along with
the data-structure changes needed for state management needed by these
functions.
CheckMD5Auth(), CheckPasswordAuth(), CheckSASLAuth(), scram_init(), verify_client_proof(),

pg_be_scram_build_secret() now uses the passed-in salt to generate the
hash. If one is not provided, then it generates a new random salt, like
before. Similarly, +encrypt_password(), pg_md5_encrypt(), and
pg_be_scram_build_secret() now accept a salt for password hashing.
---
 src/backend/commands/user.c    | 112 +++++++++++++++--
 src/backend/libpq/auth-sasl.c  |  18 +--
 src/backend/libpq/auth-scram.c | 215 ++++++++++++++++++++-------------
 src/backend/libpq/auth.c       | 115 ++++++++++++------
 src/backend/libpq/crypt.c      |  85 ++++++++++---
 src/common/scram-common.c      |   2 +-
 src/include/libpq/crypt.h      |   6 +-
 src/include/libpq/sasl.h       |   4 +-
 src/include/libpq/scram.h      |   2 +-
 9 files changed, 393 insertions(+), 166 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 32711f413d..54f9b3fb42 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -30,7 +30,9 @@
 #include "commands/defrem.h"
 #include "commands/seclabel.h"
 #include "commands/user.h"
+#include "common/scram-common.h"
 #include "libpq/crypt.h"
+#include "libpq/scram.h"
 #include "miscadmin.h"
 #include "storage/lmgr.h"
 #include "utils/acl.h"
@@ -124,6 +126,86 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+/*
+ * Inspect the current passwords of a role, and return the salt that can be used
+ * for hashing of newer passwords.
+ *
+ * Returns success on error, and false otherwise. On error the reason is stored in
+ * logdetail. On success, salt may be null which indicates that the caller is
+ * free to generate a new salt.
+ */
+static bool
+get_salt(char *rolename, char **salt, const char **logdetail)
+{
+	char	  **current_secrets;
+	int			i, num_secrets;
+	char	   *salt1, *salt2 = NULL;
+	PasswordType passtype;
+
+	if (Password_encryption == PASSWORD_TYPE_MD5)
+	{
+		*salt = rolename; /* md5 always uses role name, no need to look through the passwords */
+		return true;
+	}
+	else if (Password_encryption == PASSWORD_TYPE_PLAINTEXT)
+	{
+		*salt = NULL; /* Plaintext does not have a salt */
+		return true;
+	}
+
+	current_secrets = get_role_passwords(rolename, logdetail, &num_secrets);
+	if (num_secrets == 0)
+	{
+		*salt = NULL; /* No existing passwords, allow salt to be generated */
+		return true;
+	}
+
+	for (i = 0; i < num_secrets; i++)
+	{
+		passtype = get_password_type(current_secrets[i]);
+
+		if (passtype == PASSWORD_TYPE_MD5 || passtype == PASSWORD_TYPE_PLAINTEXT)
+			continue; /* md5 uses rolename as salt so it is always the same, and plaintext has no salt */
+		else if (passtype == PASSWORD_TYPE_SCRAM_SHA_256)
+		{
+				int			iterations;
+				int			key_length = 0;
+				pg_cryptohash_type hash_type;
+				uint8		stored_key[SCRAM_MAX_KEY_LEN];
+				uint8		server_key[SCRAM_MAX_KEY_LEN];
+
+				if (!parse_scram_secret(current_secrets[i], &iterations, &hash_type, &key_length,
+										&salt1, stored_key, server_key))
+				{
+						*logdetail = psprintf(_("could not parse SCRAM secret"));
+						*salt = NULL;
+						return false;
+				}
+
+				if (salt2 != NULL)
+				{
+					if (strcmp(salt1, salt2))
+					{
+						*logdetail = psprintf(_("inconsistent salts, clearing password")); // TODO: Better message
+						*salt = NULL;
+						return false;
+					}
+				}
+				else
+					salt2 = salt1;
+		}
+	}
+
+	for (i = 0; i < num_secrets; i++)
+		pfree(current_secrets[i]);
+	if (current_secrets)
+		pfree(current_secrets);
+
+	if (salt2)
+		*salt = pstrdup(salt2);
+
+	return true;
+}
 
 /*
  * CREATE ROLE
@@ -152,8 +234,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	List	   *addroleto = NIL;	/* roles to make this a member of */
 	List	   *rolemembers = NIL;	/* roles to be members of this role */
 	List	   *adminmembers = NIL; /* roles to be admins of this role */
-	char	   *validUntil = NULL;	/* time the login is valid until */
-	Datum		validUntil_datum;	/* same, as timestamptz Datum */
+	char	   *validUntil = NULL;	/* time the password is valid until */
+	Datum		validUntil_datum;	/* validuntil, as timestamptz Datum */
 	bool		validUntil_null;
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
@@ -441,11 +523,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
+			char *salt;
+
+			if (!get_salt(stmt->role, &salt, &logdetail))
+				ereport(ERROR,
+						(errmsg("could not get a valid salt for password"),
+						errdetail("%s", logdetail)));
+
 			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, stmt->role,
-										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
-				CStringGetTextDatum(shadow_pass);
+			shadow_pass = encrypt_password(Password_encryption, salt, password);
+			new_record[Anum_pg_authid_rolpassword - 1] = CStringGetTextDatum(shadow_pass);
 		}
 	}
 	else
@@ -771,7 +858,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 						   "SUPERUSER", "SUPERUSER")));
 
 	/*
-	 * Most changes to a role require that you both have CREATEROLE privileges
+	 * Most changes to a role require that you have both CREATEROLE privileges
 	 * and also ADMIN OPTION on the role.
 	 */
 	if (!have_createrole_privilege() ||
@@ -930,9 +1017,16 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else
 		{
+			char	   *salt;
+
+			if (!get_salt(rolename, &salt, &logdetail))
+				ereport(ERROR,
+						(errcode(ERRCODE_INTERNAL_ERROR),
+						errmsg("could not get a valid salt for password"),
+						errdetail("%s", logdetail)));
+
 			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
+			shadow_pass = encrypt_password(Password_encryption, salt, password);
 			new_record[Anum_pg_authid_rolpassword - 1] =
 				CStringGetTextDatum(shadow_pass);
 		}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 08b24d90b4..0c852cad00 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -32,11 +32,11 @@
  * Perform a SASL exchange with a libpq client, using a specific mechanism
  * implementation.
  *
- * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
- * shadowed passwords, a NULL pointer here means that an entry could not
- * be found for the role (or the user does not exist), and the mechanism
- * should fail the authentication exchange.
+ * passwords is an optional pointer to the stored secrets of the role
+ * authenticated, from pg_authid's rolpassword and rolsecondpassword.  For
+ * mechanisms that use shadowed passwords, a NULL pointer here means that an
+ * entry could not be found for the role (or the user does not exist), and the
+ * mechanism should fail the authentication exchange.
  *
  * Mechanisms must take care not to reveal to the client that a user entry
  * does not exist; ideally, the external failure mode is identical to that
@@ -45,11 +45,11 @@
  * assist debugging by the server admin.
  *
  * A mechanism is not required to utilize a shadow entry, or even a password
- * system at all; for these cases, shadow_pass may be ignored and the caller
- * should just pass NULL.
+ * system at all; for these cases, passwords paramter may be ignored and the
+ * caller should just pass NULL.
  */
 int
-CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
+CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, const char **passwords, int num_passwords,
 			  const char **logdetail)
 {
 	StringInfoData sasl_mechs;
@@ -136,7 +136,7 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
 			 * This is because we don't want to reveal to an attacker what
 			 * usernames are valid, nor which users have a valid password.
 			 */
-			opaq = mech->init(port, selected_mech, shadow_pass);
+			opaq = mech->init(port, selected_mech, passwords, num_passwords);
 
 			inputlen = pq_getmsgint(&buf, 4);
 			if (inputlen == -1)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 4161959914..f23fd9e88b 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -104,7 +104,7 @@
 
 static void scram_get_mechanisms(Port *port, StringInfo buf);
 static void *scram_init(Port *port, const char *selected_mech,
-						const char *shadow_pass);
+						const char **secrets, const int num_secrets);
 static int	scram_exchange(void *opaq, const char *input, int inputlen,
 						   char **output, int *outputlen,
 						   const char **logdetail);
@@ -127,6 +127,12 @@ typedef enum
 	SCRAM_AUTH_FINISHED,
 } scram_state_enum;
 
+typedef struct
+{
+	uint8		StoredKey[SCRAM_MAX_KEY_LEN];
+	uint8		ServerKey[SCRAM_MAX_KEY_LEN];
+} scram_secret;
+
 typedef struct
 {
 	scram_state_enum state;
@@ -140,10 +146,16 @@ typedef struct
 	pg_cryptohash_type hash_type;
 	int			key_length;
 
+	/*
+	 * The salt and iterations must be the same for all
+	 * secrets since they are sent as part of the initial message
+	 */
 	int			iterations;
 	char	   *salt;			/* base64-encoded */
-	uint8		StoredKey[SCRAM_MAX_KEY_LEN];
-	uint8		ServerKey[SCRAM_MAX_KEY_LEN];
+	/* Array of possible secrets */
+	scram_secret *secrets;
+	int			num_secrets;
+	int			chosen_secret; /* secret chosen during final client message */
 
 	/* Fields of the first message from client */
 	char		cbind_flag;
@@ -226,17 +238,20 @@ scram_get_mechanisms(Port *port, StringInfo buf)
  * It should be one of the mechanisms that we support, as returned by
  * scram_get_mechanisms().
  *
- * 'shadow_pass' is the role's stored secret, from pg_authid.rolpassword.
- * The username was provided by the client in the startup message, and is
- * available in port->user_name.  If 'shadow_pass' is NULL, we still perform
- * an authentication exchange, but it will fail, as if an incorrect password
- * was given.
+ * 'passwords' are the role's stored secrets, from pg_authid's rolpassword and
+ * rolsecondpassword columns.  The username was provided by the client in the
+ * startup message, and is available in port->user_name.  If 'shadow_pass' is
+ * NULL, we still perform an authentication exchange, but it will fail, as if an
+ * incorrect password was given.
  */
 static void *
-scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
+scram_init(Port *port, const char *selected_mech, const char **secrets, const int num_secrets)
 {
 	scram_state *state;
-	bool		got_secret;
+	bool		got_secret = false;
+	int			i;
+	int	iterations;
+	char *salt = NULL;			/* base64-encoded */
 
 	state = (scram_state *) palloc0(sizeof(scram_state));
 	state->port = port;
@@ -265,49 +280,54 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	/*
 	 * Parse the stored secret.
 	 */
-	if (shadow_pass)
+	if (secrets)
 	{
-		int			password_type = get_password_type(shadow_pass);
-
-		if (password_type == PASSWORD_TYPE_SCRAM_SHA_256)
+		state->secrets = palloc0(sizeof(scram_secret) * num_secrets);
+		state->num_secrets = num_secrets;
+		for (i = 0; i < num_secrets; i++)
 		{
-			if (parse_scram_secret(shadow_pass, &state->iterations,
-								   &state->hash_type, &state->key_length,
-								   &state->salt,
-								   state->StoredKey,
-								   state->ServerKey))
-				got_secret = true;
-			else
+			int			password_type = get_password_type(secrets[i]);
+
+			if (password_type == PASSWORD_TYPE_SCRAM_SHA_256)
 			{
-				/*
-				 * The password looked like a SCRAM secret, but could not be
-				 * parsed.
-				 */
-				ereport(LOG,
-						(errmsg("invalid SCRAM secret for user \"%s\"",
-								state->port->user_name)));
-				got_secret = false;
+				if (parse_scram_secret(secrets[i], &state->iterations,
+									   &state->hash_type, &state->key_length,
+									   &state->salt,
+									   state->secrets[i].StoredKey,
+									   state->secrets[i].ServerKey))
+				{
+					if (salt)
+					{
+						/* The stored iterations and salt must match or we cannot proceed, allow failure via mock */
+						if (strcmp(salt, state->salt) || iterations != state->iterations)
+						{
+							ereport(WARNING, (errmsg("inconsistent salt or iterations for user \"%s\"",
+														state->port->user_name)));
+							got_secret = false; /* fail and allow mock creditials to be created */
+							pfree(state->secrets);
+							state->num_secrets = 0;
+							break;
+						}
+					}
+					else
+					{
+						salt = state->salt;
+						iterations = state->iterations;
+						got_secret = true; /* We got at least one good SCRAM secret */
+					}
+				}
+				else
+				{
+					/*
+					* The password looked like a SCRAM secret, but could not be
+					* parsed.
+					*/
+					ereport(LOG,
+							(errmsg("invalid SCRAM secret for user \"%s\"",
+									state->port->user_name)));
+				}
 			}
 		}
-		else
-		{
-			/*
-			 * The user doesn't have SCRAM secret. (You cannot do SCRAM
-			 * authentication with an MD5 hash.)
-			 */
-			state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM secret."),
-										state->port->user_name);
-			got_secret = false;
-		}
-	}
-	else
-	{
-		/*
-		 * The caller requested us to perform a dummy authentication.  This is
-		 * considered normal, since the caller requested it, so don't set log
-		 * detail.
-		 */
-		got_secret = false;
 	}
 
 	/*
@@ -318,10 +338,13 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 */
 	if (!got_secret)
 	{
+		state->secrets = palloc0(sizeof(scram_secret));
+		state->num_secrets = 1;
+
 		mock_scram_secret(state->port->user_name, &state->hash_type,
 						  &state->iterations, &state->key_length,
 						  &state->salt,
-						  state->StoredKey, state->ServerKey);
+						  state->secrets[0].StoredKey, state->secrets[0].ServerKey);
 		state->doomed = true;
 	}
 
@@ -464,12 +487,13 @@ scram_exchange(void *opaq, const char *input, int inputlen,
 }
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_authid's rolpassword or
+ * rolsecondpassword.
  *
  * The result is palloc'd, so caller is responsible for freeing it.
  */
 char *
-pg_be_scram_build_secret(const char *password)
+pg_be_scram_build_secret(const char *password, const char *salt)
 {
 	char	   *prep_password;
 	pg_saslprep_rc rc;
@@ -486,11 +510,20 @@ pg_be_scram_build_secret(const char *password)
 	if (rc == SASLPREP_SUCCESS)
 		password = (const char *) prep_password;
 
-	/* Generate random salt */
-	if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
+	/* Use passed-in salt, or generate random salt */
+	if (!salt && !pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
+	{
 		ereport(ERROR,
 				(errcode(ERRCODE_INTERNAL_ERROR),
 				 errmsg("could not generate random salt")));
+	}
+	else if (salt)
+	{
+		if (pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN) == -1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INTERNAL_ERROR),
+					errmsg("could not decode SCRAM salt")));
+	}
 
 	result = scram_build_secret(PG_SHA256, SCRAM_SHA_256_KEY_LEN,
 								saltbuf, SCRAM_DEFAULT_SALT_LEN,
@@ -1137,48 +1170,62 @@ verify_client_proof(scram_state *state)
 	uint8		ClientSignature[SCRAM_MAX_KEY_LEN];
 	uint8		ClientKey[SCRAM_MAX_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_MAX_KEY_LEN];
-	pg_hmac_ctx *ctx = pg_hmac_create(state->hash_type);
-	int			i;
+	pg_hmac_ctx *ctx;
+	int			i, j;
 	const char *errstr = NULL;
-
 	/*
 	 * Calculate ClientSignature.  Note that we don't log directly a failure
 	 * here even when processing the calculations as this could involve a mock
 	 * authentication.
 	 */
-	if (pg_hmac_init(ctx, state->StoredKey, state->key_length) < 0 ||
-		pg_hmac_update(ctx,
-					   (uint8 *) state->client_first_message_bare,
-					   strlen(state->client_first_message_bare)) < 0 ||
-		pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
-		pg_hmac_update(ctx,
-					   (uint8 *) state->server_first_message,
-					   strlen(state->server_first_message)) < 0 ||
-		pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
-		pg_hmac_update(ctx,
-					   (uint8 *) state->client_final_message_without_proof,
-					   strlen(state->client_final_message_without_proof)) < 0 ||
-		pg_hmac_final(ctx, ClientSignature, state->key_length) < 0)
+	for (j = 0; j < state->num_secrets; j++)
 	{
-		elog(ERROR, "could not calculate client signature: %s",
-			 pg_hmac_error(ctx));
-	}
+		ctx = pg_hmac_create(state->hash_type);
+		elog(LOG, "Trying to verify password %d", j); // TODO: Convert to DEBUG2
+
+		if (pg_hmac_init(ctx, state->secrets[j].StoredKey, state->key_length) < 0 ||
+			pg_hmac_update(ctx,
+						(uint8 *) state->client_first_message_bare,
+						strlen(state->client_first_message_bare)) < 0 ||
+			pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
+			pg_hmac_update(ctx,
+						(uint8 *) state->server_first_message,
+						strlen(state->server_first_message)) < 0 ||
+			pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 ||
+			pg_hmac_update(ctx,
+						(uint8 *) state->client_final_message_without_proof,
+						strlen(state->client_final_message_without_proof)) < 0 ||
+			pg_hmac_final(ctx, ClientSignature, state->key_length) < 0)
+		{
+			// TODO: Convert to DEBUG2
+			elog(LOG, "could not calculate client signature for secret %d", j);
+			pg_hmac_free(ctx);
+			continue;
+		}
 
-	pg_hmac_free(ctx);
+		// TODO: Convert to DEBUG2
+		elog(LOG, "succeeded on %d password", j);
 
-	/* Extract the ClientKey that the client calculated from the proof */
-	for (i = 0; i < state->key_length; i++)
-		ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
+		pg_hmac_free(ctx);
 
-	/* Hash it one more time, and compare with StoredKey */
-	if (scram_H(ClientKey, state->hash_type, state->key_length,
-				client_StoredKey, &errstr) < 0)
-		elog(ERROR, "could not hash stored key: %s", errstr);
+		/* Extract the ClientKey that the client calculated from the proof */
+		for (i = 0; i < state->key_length; i++)
+			ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
-	if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0)
-		return false;
+		/* Hash it one more time, and compare with StoredKey */
+		if (scram_H(ClientKey, state->hash_type, state->key_length,
+					client_StoredKey, &errstr) < 0)
+			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	return true;
+		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, state->key_length) == 0) {
+			// TODO: Convert to DEBUG2
+			elog(LOG, "Moving forward with Password %d", j);
+			state->chosen_secret = j;
+			return true;
+		}
+	}
+
+	return false;
 }
 
 /*
@@ -1404,7 +1451,7 @@ build_server_final_message(scram_state *state)
 	pg_hmac_ctx *ctx = pg_hmac_create(state->hash_type);
 
 	/* calculate ServerSignature */
-	if (pg_hmac_init(ctx, state->ServerKey, state->key_length) < 0 ||
+	if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, state->key_length) < 0 ||
 		pg_hmac_update(ctx,
 					   (uint8 *) state->client_first_message_bare,
 					   strlen(state->client_first_message_bare)) < 0 ||
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c5270..3f18c3bb0c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -55,8 +55,7 @@ static void set_authn_id(Port *port, const char *id);
 static int	CheckPasswordAuth(Port *port, const char **logdetail);
 static int	CheckPWChallengeAuth(Port *port, const char **logdetail);
 
-static int	CheckMD5Auth(Port *port, char *shadow_pass,
-						 const char **logdetail);
+static int	CheckMD5Auth(Port *port, const char **passwords, int num_passwords, const char **logdetail);
 
 
 /*----------------------------------------------------------------
@@ -787,8 +786,9 @@ static int
 CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
-	int			result;
-	char	   *shadow_pass;
+	int			result = STATUS_ERROR;
+	int			i, num_passwords;
+	char	   **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -796,17 +796,22 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	shadow_pass = get_role_password(port->user_name, logdetail);
-	if (shadow_pass)
+	passwords = get_role_passwords(port->user_name, logdetail, &num_passwords);
+	if (passwords != NULL)
 	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
+		for (i = 0; i < num_passwords; i++)
+		{
+			result = plain_crypt_verify(port->user_name, passwords[i], passwd,
+										logdetail);
+			if (result == STATUS_OK)
+				break; /* Found a matching password, no need to try any others */
+		}
+		for (i = 0; i < num_passwords; i++)
+			pfree(passwords[i]);
+
+		pfree(passwords);
 	}
-	else
-		result = STATUS_ERROR;
 
-	if (shadow_pass)
-		pfree(shadow_pass);
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -821,49 +826,81 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 static int
 CheckPWChallengeAuth(Port *port, const char **logdetail)
 {
-	int			auth_result;
-	char	   *shadow_pass;
-	PasswordType pwtype;
+	bool		scram_pw_avail = false;
+	int			auth_result = STATUS_ERROR;
+	int			i, num_passwords;
+	char	  **passwords;
+	PasswordType	pwtype;
 
 	Assert(port->hba->auth_method == uaSCRAM ||
 		   port->hba->auth_method == uaMD5);
 
-	/* First look up the user's password. */
-	shadow_pass = get_role_password(port->user_name, logdetail);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num_passwords);
 
 	/*
-	 * If the user does not exist, or has no password or it's expired, we
-	 * still go through the motions of authentication, to avoid revealing to
+	 * If the user does not exist, or has no passwords or they're all expired,
+	 * we still go through the motions of authentication, to avoid revealing to
 	 * the client that the user didn't exist.  If 'md5' is allowed, we choose
 	 * whether to use 'md5' or 'scram-sha-256' authentication based on current
 	 * password_encryption setting.  The idea is that most genuine users
 	 * probably have a password of that type, and if we pretend that this user
 	 * had a password of that type, too, it "blends in" best.
 	 */
-	if (!shadow_pass)
+	if (!passwords)
 		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
 
 	/*
 	 * If 'md5' authentication is allowed, decide whether to perform 'md5' or
 	 * 'scram-sha-256' authentication based on the type of password the user
-	 * has.  If it's an MD5 hash, we must do MD5 authentication, and if it's a
-	 * SCRAM secret, we must do SCRAM authentication.
+	 * has.  If there's a SCRAM password available then we'll do SCRAM, otherwise we
+	 * will fall back to trying to use MD5.
 	 *
 	 * If MD5 authentication is not allowed, always use SCRAM.  If the user
 	 * had an MD5 password, CheckSASLAuth() with the SCRAM mechanism will
 	 * fail.
 	 */
-	if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
-		auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
+	if (passwords == NULL)
+	{
+		if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
+			auth_result = CheckMD5Auth(port, (const char **) NULL, 0, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port,
+										(const char **) NULL, 0, logdetail);
+	}
 	else
-		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
-									logdetail);
+	{
+		for (i = 0; i < num_passwords; i++)
+		{
+			if (get_password_type(passwords[i]) == PASSWORD_TYPE_SCRAM_SHA_256)
+			{
+				scram_pw_avail = true;
+				break;
+			}
+		}
 
-	if (shadow_pass)
-		pfree(shadow_pass);
-	else
+		if (port->hba->auth_method == uaMD5 && !scram_pw_avail)
+			auth_result = CheckMD5Auth(port, (const char **) passwords, num_passwords, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num_passwords,
+											logdetail);
+
+		for (i = 0; i < num_passwords; i++)
+		{
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(DEBUG2,
+					(errmsg("Password %d was null", i)));
+		}
+		pfree(passwords);
+	}
+
+	/*
+	 * If get_role_passwords() returned error, return error, even if the
+	 * authentication succeeded.
+	 */
+	if (!passwords)
 	{
 		/*
 		 * If get_role_password() returned error, authentication better not
@@ -879,11 +916,12 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 }
 
 static int
-CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail)
+CheckMD5Auth(Port *port, const char **passwords, int num_passwords, const char **logdetail)
 {
 	char		md5Salt[4];		/* Password salt */
 	char	   *passwd;
-	int			result;
+	int			result = STATUS_ERROR;
+	int			i;
 
 	/* include the salt to use for computing the response */
 	if (!pg_strong_random(md5Salt, 4))
@@ -899,12 +937,13 @@ CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (shadow_pass)
-		result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
+	for (i = 0; i < num_passwords; i++)
+	{
+		result = md5_crypt_verify(port->user_name, passwords[i], passwd,
 								  md5Salt, 4, logdetail);
-	else
-		result = STATUS_ERROR;
-
+		if (result == STATUS_OK)
+			break;
+	}
 	pfree(passwd);
 
 	return result;
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 629e51e00b..463f081eb8 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -2,7 +2,7 @@
  *
  * crypt.c
  *	  Functions for dealing with encrypted passwords stored in
- *	  pg_authid.rolpassword.
+ *	  pg_authid's rolpassword and rolsecondpassword.
  *
  * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -26,20 +26,29 @@
 
 
 /*
- * Fetch stored password for a user, for authentication.
+ * Fetch valid stored passwords for a user, for authentication.
  *
  * On error, returns NULL, and stores a palloc'd string describing the reason,
  * for the postmaster log, in *logdetail.  The error reason should *not* be
  * sent to the client, to avoid giving away user information!
  */
-char *
-get_role_password(const char *role, const char **logdetail)
+char **
+get_role_passwords(const char *role, const char **logdetail, int *num_passwords)
 {
 	TimestampTz vuntil = 0;
+	TimestampTz second_vuntil = 0;
+	TimestampTz current_ts;
 	HeapTuple	roleTup;
 	Datum		datum;
-	bool		isnull;
-	char	   *shadow_pass;
+	Datum		second_datum;
+	bool		vuntil_isnull;
+	bool		second_vuntil_isnull;
+	bool		password_isnull;
+	bool		second_password_isnull;
+	char	   *shadow_pass = NULL;
+	char	   *second_shadow_pass = NULL;
+
+	*num_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -51,34 +60,71 @@ get_role_password(const char *role, const char **logdetail)
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+							Anum_pg_authid_rolpassword,
+							&password_isnull);
+	second_datum = SysCacheGetAttr(AUTHNAME, roleTup,
+									Anum_pg_authid_rolsecondpassword,
+									&second_password_isnull);
+	if (password_isnull && second_password_isnull)
 	{
 		ReleaseSysCache(roleTup);
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
+
+	if (!password_isnull)
+		shadow_pass = TextDatumGetCString(datum);
+	if (!second_password_isnull)
+		second_shadow_pass = TextDatumGetCString(second_datum);
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
-	if (!isnull)
+							Anum_pg_authid_rolvaliduntil, &vuntil_isnull);
+	second_datum = SysCacheGetAttr(AUTHNAME, roleTup,
+							Anum_pg_authid_rolsecondvaliduntil,
+							&second_vuntil_isnull);
+	if (!vuntil_isnull)
 		vuntil = DatumGetTimestampTz(datum);
+	if (!second_vuntil_isnull)
+		second_vuntil = DatumGetTimestampTz(second_datum);
 
 	ReleaseSysCache(roleTup);
 
 	/*
 	 * Password OK, but check to be sure we are not past rolvaliduntil
 	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	current_ts = GetCurrentTimestamp();
+	*num_passwords = (!password_isnull &&
+						(vuntil_isnull || vuntil >= current_ts))
+					+ (!second_password_isnull &&
+						(second_vuntil_isnull || second_vuntil >= current_ts));
+
+	if (*num_passwords >= 1)
+	{
+		int i = 0;
+		char **passwords = palloc(sizeof(char *) * (*num_passwords));
+
+		if (!password_isnull && (vuntil_isnull || vuntil >= current_ts))
+		{
+			passwords[i] = shadow_pass;
+			i++;
+		}
+
+		if (!second_password_isnull &&
+			(second_vuntil_isnull || second_vuntil >= current_ts))
+		{
+			passwords[i] = second_shadow_pass;
+			i++;
+		}
+
+		return passwords;
+	}
+	else
 	{
 		*logdetail = psprintf(_("User \"%s\" has an expired password."),
 							  role);
 		return NULL;
 	}
-
-	return shadow_pass;
 }
 
 /*
@@ -112,7 +158,7 @@ get_password_type(const char *shadow_pass)
  * hash, so it is stored as it is regardless of the requested type.
  */
 char *
-encrypt_password(PasswordType target_type, const char *role,
+encrypt_password(PasswordType target_type, const char *salt,
 				 const char *password)
 {
 	PasswordType guessed_type = get_password_type(password);
@@ -133,13 +179,13 @@ encrypt_password(PasswordType target_type, const char *role,
 		case PASSWORD_TYPE_MD5:
 			encrypted_password = palloc(MD5_PASSWD_LEN + 1);
 
-			if (!pg_md5_encrypt(password, role, strlen(role),
+			if (!pg_md5_encrypt(password, salt, strlen(salt),
 								encrypted_password, &errstr))
 				elog(ERROR, "password encryption failed: %s", errstr);
 			return encrypted_password;
 
 		case PASSWORD_TYPE_SCRAM_SHA_256:
-			return pg_be_scram_build_secret(password);
+			return pg_be_scram_build_secret(password, salt);
 
 		case PASSWORD_TYPE_PLAINTEXT:
 			elog(ERROR, "cannot encrypt password with 'plaintext'");
@@ -157,7 +203,7 @@ encrypt_password(PasswordType target_type, const char *role,
  * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
  *
  * 'shadow_pass' is the user's correct password or password hash, as stored
- * in pg_authid.rolpassword.
+ * in pg_authid's rolpassword or rolsecondpassword.
  * 'client_pass' is the response given by the remote user to the MD5 challenge.
  * 'md5_salt' is the salt used in the MD5 authentication challenge.
  *
@@ -212,7 +258,8 @@ md5_crypt_verify(const char *role, const char *shadow_pass,
  * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
  *
  * 'shadow_pass' is the user's correct password hash, as stored in
- * pg_authid.rolpassword.
+ * pg_authid's rolpassword or rolsecondpassword.
+ *
  * 'client_pass' is the password given by the remote user.
  *
  * In the error case, store a string at *logdetail that will be sent to the
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index b611bb8fe7..84b36099a1 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -196,7 +196,7 @@ scram_ServerKey(const uint8 *salted_password,
 
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_authid's rolpassword or rolsecondpassword.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f744de4d20..08d8998da2 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,8 +21,8 @@
  * Plaintext passwords can be passed in by the user, in a CREATE/ALTER USER
  * command. They will be encrypted to MD5 or SCRAM-SHA-256 format, before
  * storing on-disk, so only MD5 and SCRAM-SHA-256 passwords should appear
- * in pg_authid.rolpassword. They are also the allowed values for the
- * password_encryption GUC.
+ * in pg_authid's rolpassword and rolsecondpassword. They are also the allowed
+ * values for the password_encryption GUC.
  */
 typedef enum PasswordType
 {
@@ -35,7 +35,7 @@ extern PasswordType get_password_type(const char *shadow_pass);
 extern char *encrypt_password(PasswordType target_type, const char *role,
 							  const char *password);
 
-extern char *get_role_password(const char *role, const char **logdetail);
+extern char **get_role_passwords(const char *role, const char **logdetail, int *num);
 
 extern int	md5_crypt_verify(const char *role, const char *shadow_pass,
 							 const char *client_pass, const char *md5_salt,
diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h
index 7a1f970cca..be2cdc0639 100644
--- a/src/include/libpq/sasl.h
+++ b/src/include/libpq/sasl.h
@@ -77,7 +77,7 @@ typedef struct pg_be_sasl_mech
 	 *				 disclosing valid user names.
 	 *---------
 	 */
-	void	   *(*init) (Port *port, const char *mech, const char *shadow_pass);
+	void	   *(*init) (Port *port, const char *mech, const char **secrets, const int num_secrets);
 
 	/*---------
 	 * exchange()
@@ -131,6 +131,6 @@ typedef struct pg_be_sasl_mech
 
 /* Common implementation for auth.c */
 extern int	CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port,
-						  char *shadow_pass, const char **logdetail);
+						  const char **passwords, int num_passwords, const char **logdetail);
 
 #endif							/* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 2ae9010557..9e8d36f58a 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -25,7 +25,7 @@ extern PGDLLIMPORT int scram_sha_256_iterations;
 extern PGDLLIMPORT const pg_be_sasl_mech pg_be_scram_mech;
 
 /* Routines to handle and check SCRAM-SHA-256 secret */
-extern char *pg_be_scram_build_secret(const char *password);
+extern char *pg_be_scram_build_secret(const char *password, const char *salt);
 extern bool parse_scram_secret(const char *secret,
 							   int *iterations,
 							   pg_cryptohash_type *hash_type,
-- 
2.25.1



  [application/octet-stream] v5-0005-Update-system-views-pg_roles-and-pg_shadow.patch (4.3K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/6-v5-0005-Update-system-views-pg_roles-and-pg_shadow.patch)
  download | inline diff:
From 8b18b5003ee6f77769c91952aa231802b5069513 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 21:17:12 -0700
Subject: [PATCH v5 5/9] Update system views pg_roles and pg_shadow

---
 doc/src/sgml/system-views.sgml       | 39 ++++++++++++++++++++++++++++
 src/backend/catalog/system_views.sql |  4 +++
 src/test/regress/expected/rules.out  |  4 +++
 3 files changed, 47 insertions(+)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7ed617170f..452edae0be 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2739,6 +2739,25 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rolsecondpassword</structfield> <type>text</type>
+      </para>
+      <para>
+       Not the second password (always reads as <literal>********</literal>)
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rolsecondvaliduntil</structfield> <type>timestamptz</type>
+      </para>
+      <para>
+       Second password's expiry time (only used for password authentication);
+       null if no expiration
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>rolbypassrls</structfield> <type>bool</type>
@@ -3570,6 +3589,26 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>secondpasswd</structfield> <type>text</type>
+      </para>
+      <para>
+       Second password (possibly encrypted); null if none.  See
+       <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>
+       for details of how encrypted passwords are stored.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>secondvaluntil</structfield> <type>timestamptz</type>
+      </para>
+      <para>
+       Second password's expiry time (only used for password authentication)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>useconfig</structfield> <type>text[]</type>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 2e61f6d74e..e55364c3bd 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -26,6 +26,8 @@ CREATE VIEW pg_roles AS
         rolconnlimit,
         '********'::text as rolpassword,
         rolvaliduntil,
+        '********'::text as rolsecondpassword,
+        rolsecondvaliduntil,
         rolbypassrls,
         setconfig as rolconfig,
         pg_authid.oid
@@ -42,6 +44,8 @@ CREATE VIEW pg_shadow AS
         rolbypassrls AS usebypassrls,
         rolpassword AS passwd,
         rolvaliduntil AS valuntil,
+        rolsecondpassword AS secondpasswd,
+        rolsecondvaliduntil AS secondvaluntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f4a0f36377..7af39be2ac 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1490,6 +1490,8 @@ pg_roles| SELECT pg_authid.rolname,
     pg_authid.rolconnlimit,
     '********'::text AS rolpassword,
     pg_authid.rolvaliduntil,
+    '********'::text AS rolsecondpassword,
+    pg_authid.rolsecondvaliduntil,
     pg_authid.rolbypassrls,
     s.setconfig AS rolconfig,
     pg_authid.oid
@@ -1733,6 +1735,8 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolbypassrls AS usebypassrls,
     pg_authid.rolpassword AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
+    pg_authid.rolsecondpassword AS secondpasswd,
+    pg_authid.rolsecondvaliduntil AS secondvaluntil,
     s.setconfig AS useconfig
    FROM (pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
-- 
2.25.1



  [application/octet-stream] v5-0007-Updated-psql-s-describe-roles-meta-command.patch (837B, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/7-v5-0007-Updated-psql-s-describe-roles-meta-command.patch)
  download | inline diff:
From f20a487f6bab8bb1e0a6c42d5a9af8edb8d77486 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 21:41:51 -0700
Subject: [PATCH v5 7/9] Updated psql's describe-roles meta-command

---
 src/bin/psql/describe.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 6433497bcd..af00b52eca 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3683,7 +3683,7 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 	printfPQExpBuffer(&buf,
 					  "SELECT r.rolname, r.rolsuper, r.rolinherit,\n"
 					  "  r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\n"
-					  "  r.rolconnlimit, r.rolvaliduntil");
+					  "  r.rolconnlimit, r.rolvaliduntil, r.rolsecondvaliduntil");
 
 	if (verbose)
 	{
-- 
2.25.1



  [application/octet-stream] v5-0006-Updated-pg_authid-catalog-documentation.patch (1.8K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/8-v5-0006-Updated-pg_authid-catalog-documentation.patch)
  download | inline diff:
From 470b5f2c369670af0c130194ffa47f7210b2e7c9 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Mon, 9 Oct 2023 21:38:06 -0700
Subject: [PATCH v5 6/9] Updated pg_authid catalog documentation

---
 doc/src/sgml/catalogs.sgml | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 096ddab481..aae349fb15 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1605,12 +1605,39 @@
        null if no expiration
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rolsecondpassword</structfield> <type>text</type>
+      </para>
+      <para>
+       Second password (possibly encrypted); null if none. The format depends
+       on the form of encryption used.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rolsecondvaliduntil</structfield> <type>timestamptz</type>
+      </para>
+      <para>
+       Second password's expiry time (only used for password authentication);
+       null if no expiration
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
 
+
+  <para>
+   <structfield>rolpassword</structfield> and <structfield>rolsecondpassword</structfield>
+   store either the unencrypted password, MD5 encrypted password, or
+   SCRAM-SHA-256 encrypted password.
+  </para>
+
   <para>
-   For an MD5 encrypted password, <structfield>rolpassword</structfield>
+   For an MD5 encrypted password, the
    column will begin with the string <literal>md5</literal> followed by a
    32-character hexadecimal MD5 hash. The MD5 hash will be of the user's
    password concatenated to their user name. For example, if user
-- 
2.25.1



  [application/octet-stream] v5-0008-Added-documentation-for-ALTER-ROLE-command.patch (3.8K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/9-v5-0008-Added-documentation-for-ALTER-ROLE-command.patch)
  download | inline diff:
From 6686a7d98eb65237228aa9d790bb6e199ff9bca3 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Tue, 10 Oct 2023 01:14:49 -0700
Subject: [PATCH v5 8/9] Added documentation for ALTER ROLE command

---
 doc/src/sgml/ref/alter_role.sgml | 47 ++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 7b0a04bc46..933bdab5c5 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -35,6 +35,9 @@ ALTER ROLE <replaceable class="parameter">role_specification</replaceable> [ WIT
     | CONNECTION LIMIT <replaceable class="parameter">connlimit</replaceable>
     | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' | PASSWORD NULL
     | VALID UNTIL '<replaceable class="parameter">timestamp</replaceable>'
+    | ADD { FIRST | SECOND } PASSWORD '<replaceable class="parameter">password</replaceable>'
+    | DROP { FIRST | SECOND | ALL } PASSWORD
+    | { FIRST | SECOND } PASSWORD VALID UNTIL '<replaceable class="parameter">timestamp</replaceable>'
 
 ALTER ROLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable>
 
@@ -128,6 +131,14 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
    set if a superuser issues the command.  Only superusers can change a setting
    for all roles in all databases.
   </para>
+
+  <para>
+   To support gradual password rollovers, PostgreSQL provides the ability to
+   store up to two passwords at the same time for each role. These passwords are
+   referred to as <literal>FIRST</literal> and <literal>SECOND</literal>
+   password. Each of these passwords can be changed independently, and each of
+   these can have their own password expiration time.
+  </para>
  </refsect1>
 
  <refsect1 id="sql-alterrole-params">
@@ -191,6 +202,34 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
       </listitem>
      </varlistentry>
 
+     <varlistentry id="sql-alterrole-params-add-password">
+      <term><literal>ADD</literal> { <literal>FIRST</literal> | <literal>SECOND</literal> } <literal>PASSWORD</literal> '<replaceable class="parameter">password</replaceable>'</term>
+      <listitem>
+       <para>
+        Set the first, or the second, password of the role. It is an error if the
+        corresponding password is already set.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="sql-alterrole-params-drop-password">
+      <term><literal>DROP</literal> { <literal>FIRST</literal> | <literal>SECOND</literal> | <literal>ALL</literal> } <literal>PASSWORD</literal> </term>
+      <listitem>
+       <para>
+        Clear the first, the second, or all passwords of the role.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="sql-alterrole-params-first-password-valid-until">
+      <term> { <literal>FIRST</literal> | <literal>SECOND</literal> } <literal>PASSWORD VALID UNTIL</literal> '<replaceable class="parameter">timestamp</replaceable>'</term>
+      <listitem>
+       <para>
+        Sets a date and time after which the corresponding password is no longer valid.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="sql-alterrole-params-new-name">
       <term><replaceable>new_name</replaceable></term>
       <listitem>
@@ -337,6 +376,14 @@ ALTER ROLE worker_bee SET maintenance_work_mem = 100000;
 <programlisting>
 ALTER ROLE fred IN DATABASE devel SET client_min_messages = DEBUG;
 </programlisting></para>
+
+  <para>
+   Add a second password to a role:
+
+<programlisting>
+ALTER ROLE fred ADD SECOND PASSwORD 'secret' SECOND PASSWORD VALID UNTIL '2005/01/01';
+</programlisting>
+  </para>
  </refsect1>
 
  <refsect1 id="sql-alterrole-compat">
-- 
2.25.1



  [application/octet-stream] v5-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patch (2.2K, ../CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com/10-v5-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patch)
  download | inline diff:
From ed6680897e5087fac2b1b8bd78d83ce8f5ce10ac Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <[email protected]>
Date: Tue, 10 Oct 2023 02:07:57 -0700
Subject: [PATCH v5 9/9] Added TAP tests to prove that a role can use two
 passwords to login

---
 src/test/authentication/t/001_password.pl | 52 +++++++++++++++++++++++
 1 file changed, 52 insertions(+)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 87e180af3d..25ebcc91e1 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -694,4 +694,56 @@ test_conn(
 		qr/connection authenticated: identity="regress_not_member" method=scram-sha-256/
 	]);
 
+# Create roles, and assign two passwords for password rollover tests
+reset_pg_hba($node, 'all', 'all', 'trust');
+$node->safe_psql(
+	'postgres',
+	qq{set password_encryption = 'scram-sha-256';
+CREATE ROLE regress_password_rollover_scram LOGIN PASSWORD 'scram';
+ALTER ROLE regress_password_rollover_scram ADD SECOND PASSWORD 'scram2';
+set password_encryption = 'md5';
+CREATE ROLE regress_password_rollover_md5 LOGIN PASSWORD 'md5';
+ALTER ROLE regress_password_rollover_md5 ADD SECOND PASSWORD 'md5_2';
+});
+
+reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
+$ENV{"PGPASSWORD"} = 'scram';
+test_conn(
+	$node,
+	'user=regress_password_rollover_scram',
+	'scram-sha-256',
+	0,
+	log_like => [
+		qr/connection authenticated: identity="regress_password_rollover_scram" method=scram-sha-256/
+	]);
+$ENV{"PGPASSWORD"} = 'scram2';
+test_conn(
+	$node,
+	'user=regress_password_rollover_scram',
+	'scram-sha-256',
+	0,
+	log_like => [
+		qr/connection authenticated: identity="regress_password_rollover_scram" method=scram-sha-256/
+	]);
+
+reset_pg_hba($node, 'all', 'all', 'md5');
+$ENV{"PGPASSWORD"} = 'md5';
+test_conn(
+	$node,
+	'user=regress_password_rollover_md5',
+	'md5',
+	0,
+	log_like => [
+		qr/connection authenticated: identity="regress_password_rollover_md5" method=md5/
+	]);
+$ENV{"PGPASSWORD"} = 'md5_2';
+test_conn(
+	$node,
+	'user=regress_password_rollover_md5',
+	'md5',
+	0,
+	log_like => [
+		qr/connection authenticated: identity="regress_password_rollover_md5" method=md5/
+	]);
+
 done_testing();
-- 
2.25.1



view thread (25+ messages)

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: [PoC/RFC] Multiple passwords, interval expirations
  In-Reply-To: <CALdSSPg0VuiF2yfffwphHep+bpFYG5YF66vR4OczK7kq8c+m2g@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

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