[PoC/RFC] Multiple passwords, interval expirations

Started by Joshua Brindlealmost 4 years ago32 messages
#1Joshua Brindle
joshua.brindle@crunchydata.com
3 attachment(s)

This is not intended for PG15.

Attached are a proof of concept patchset to implement multiple valid
passwords, which have independent expirations, set by a GUC or SQL
using an interval.

This allows the superuser to set a password validity period of e.g.,
60 days, and for users to create new passwords before the old ones
expire, and use both until the old one expires. This will aid in
password rollovers for apps and other systems that need to connect
with password authentication.

The first patch simply moves password to a new catalog, no functional changes.
The second patch allows multiple passwords to be used simultaneously.
The third adds per-password expiration, SQL grammar, and the GUC.

Some future work intended to build on this includes:
- disallowing password reuse
- transitioning between password mechanisms

Example output (note the NOTICES can go away, but are helpful for
demo/testing purposes):

postgres=# alter system set password_valid_duration = '1 day';
NOTICE: Setting password duration to "1 day"
ALTER SYSTEM
postgres=# select pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)

postgres=# create user joshua password 'a' expires in '5 minutes';
NOTICE: Setting password duration to "1 day"
NOTICE: Password will expire at: "2022-03-02 14:52:31.217193" (from SQL)
CREATE ROLE

---

$ psql -h 127.0.0.1 -U joshua postgres
Password for user joshua:
psql (12.7, server 15devel)
WARNING: psql major version 12, server major version 15.
Some psql features might not work.
Type "help" for help.

postgres=> alter role joshua passname 'newone' password 'asdf' expires
in '1 year';
ERROR: must be superuser to override password_validity_duration GUC
postgres=> alter role joshua passname 'newone' password 'asdf';
NOTICE: Password will expire at: "2022-03-03 14:47:53.728159" (from GUC)
ALTER ROLE
postgres=>

--

postgres=# select * from pg_auth_password ;
roleid | name |
password
| expiration
--------+---------+-------------------------------------------------------------------------------------------------------------------
--------------------+-------------------------------
10 | __def__ |
SCRAM-SHA-256$4096:yGiHIYPwc2az7xj/7TIyTA==$OQL/AEcEY1yOCNbrZEj4zDvNnOLpIqltOW1uQvosLvc=:9VRRppuIkSrwhiBN5ePy8wB1y
zDa/2uX0WUx6gXi93E= |
16384 | __def__ |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$1Ivp4d+vAWxowpuGEn05KR9lxyGOms3yy85k3D7XpBg=:k8xUjU6xrJG17PMGa/Zya6pAE
/M7pEDaoIFmWvNIEUg= | 2022-03-02 06:52:31.217193-08
16384 | newone |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$WK3+41CCGDognSnZrtpHhv00z9LuVUjHR1hWq8T1+iE=:w2C5GuhgiEB7wXqPxYfxBKB+e
hm4h6Oeif1uzpPIFVk= | 2022-03-03 06:47:53.728159-08
(3 rows)

Attachments:

0002-multiple-passwords-work-with-scram-and-md5.patchapplication/octet-stream; name=0002-multiple-passwords-work-with-scram-and-md5.patchDownload
From 6dc86ab02a1639044440e9c217c0ba649da932d9 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Tue, 1 Feb 2022 14:32:55 -0800
Subject: [PATCH 2/3] multiple passwords work with scram and md5

also renaming roles, dropping roles, switching between
password_encryption

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 194 +++++++++++++++++++++----
 src/backend/libpq/auth-sasl.c          |   4 +-
 src/backend/libpq/auth-scram.c         | 184 +++++++++++++----------
 src/backend/libpq/auth.c               |  94 +++++-------
 src/backend/libpq/crypt.c              |  50 ++++---
 src/backend/parser/gram.y              |  13 +-
 src/backend/utils/cache/catcache.c     |   2 +-
 src/backend/utils/cache/syscache.c     |   6 +-
 src/include/catalog/pg_auth_password.h |   9 +-
 src/include/commands/user.h            |   2 +-
 src/include/libpq/crypt.h              |   2 +-
 src/include/libpq/sasl.h               |   4 +-
 src/include/libpq/scram.h              |   2 +-
 src/include/parser/kwlist.h            |   1 +
 src/include/utils/syscache.h           |   2 +-
 src/test/regress/expected/password.out |   2 +-
 16 files changed, 370 insertions(+), 201 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 9ff99342eef..69daabd7928 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -18,6 +18,7 @@
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
@@ -31,11 +32,14 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/fmgroids.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -47,6 +51,9 @@ Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 /* GUC parameter */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
 
+/* default password name */
+const char* default_passname = "__def__";
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 check_password_hook_type check_password_hook = NULL;
 
@@ -69,7 +76,7 @@ have_createrole_privilege(void)
  * Is the role able to log in
 */
 bool
-is_role_valid(const char *rolename, char **logdetail)
+is_role_valid(const char *rolename, const char **logdetail)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
@@ -102,6 +109,64 @@ is_role_valid(const char *rolename, char **logdetail)
 }
 
 
+static
+bool
+validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
+{
+	char	  **current_secrets;
+	char	   *salt1, *salt2 = NULL;
+	int			i, num;
+	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);
+	if (num == 0)
+	{
+		*salt = NULL; /* No existing passwords, allow one to be generated */
+		return true;
+	}
+	for (i = 0; i < num; 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;
+				uint8		stored_key[SCRAM_KEY_LEN];
+				uint8		server_key[SCRAM_KEY_LEN];
+
+				parse_scram_secret(current_secrets[i], &iterations, &salt1,
+									stored_key, server_key);
+
+				if (salt2 != NULL) {
+					if (strcmp(salt1, salt2)) {
+						*logdetail = psprintf(_("inconsistent salts, clearing password"));
+						*salt = NULL;
+						return false;
+					}
+				}
+				else
+					salt2 = salt1;
+		}
+
+	}
+	for (i = 0; i < num; i++)
+		pfree(current_secrets[i]);
+	pfree(current_secrets);
+	if (salt2)
+		*salt = pstrdup(salt2);
+	return true;
+}
+
 /*
  * CREATE ROLE
  */
@@ -117,6 +182,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	ListCell   *item;
 	ListCell   *option;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* name of the password for managing multiple passwords */
 	bool		issuper = false;	/* Make the user a superuser? */
 	bool		inherit = true; /* Auto inherit privileges? */
 	bool		createrole = false; /* Can this user create roles? */
@@ -144,6 +210,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dadminmembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -246,6 +314,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -283,6 +358,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		validUntil = strVal(dvalidUntil->arg);
 	if (dbypassRLS)
 		bypassrls = boolVal(dbypassRLS->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/* Check some permissions first */
 	if (issuper)
@@ -425,7 +502,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	if (password)
 	{
 		char	   *shadow_pass;
-		char	   *logdetail;
+		const char	   *logdetail;
 		Datum		new_password_record[Natts_pg_auth_password];
 		bool		new_password_record_nulls[Natts_pg_auth_password];
 		Relation	pg_auth_password_rel;
@@ -458,7 +535,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 			MemSet(new_password_record, 0, sizeof(new_password_record));
 			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
-
+			if (passname != NULL)
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(passname));
+			else
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -560,6 +642,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* password name for managing multiple passwords */
 	int			connlimit = -1; /* maximum connections allowed */
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
@@ -575,6 +658,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -652,6 +736,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -669,6 +759,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -805,8 +897,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	/* password */
 	if (password)
 	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
+		char	*salt = NULL;
+		const char 	*logdetail = NULL;
+		char	*shadow_pass;
 
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (password[0] == '\0' ||
@@ -818,13 +911,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else
 		{
-			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
-			new_password_record[Anum_pg_auth_password_password - 1] =
-				CStringGetTextDatum(shadow_pass);
+			if (!validate_and_get_salt(rolename, &salt, &logdetail))
+				new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+			else
+			{
+				/* Encrypt the password to the requested format. */
+				shadow_pass = encrypt_password(Password_encryption, salt,
+											password);
+				new_password_record[Anum_pg_auth_password_password - 1] =
+					CStringGetTextDatum(shadow_pass);
+
+				new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+				if (passname)
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(passname));
+				else
+					new_password_record[Anum_pg_auth_password_name - 1] =
+					DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+			}
 		}
-		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
@@ -859,7 +965,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
-		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+		if (dpassName)
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+		else
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname));
 
 		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
 		{
@@ -1157,14 +1266,23 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
 		/*
-		 * Drop password
+		 * Drop password(s)
 		 */
-		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
-		if (HeapTupleIsValid(tuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
-			ReleaseSysCache(tuple);
+		ScanKeyInit(&scankey,
+					Anum_pg_auth_password_roleid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(roleid));
+
+		sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								   true, NULL, 1, &scankey);
+
+		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+		{
+			CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
 		}
 
+		systable_endscan(sscan);
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1201,6 +1319,9 @@ RenameRole(const char *oldname, const char *newname)
 				newtuple,
 				passtuple;
 	TupleDesc	dsc;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+
 	Relation	rel;
 	Datum		datum;
 	bool		isnull = true;
@@ -1211,6 +1332,7 @@ RenameRole(const char *oldname, const char *newname)
 	Oid			roleid;
 	ObjectAddress address;
 	Form_pg_authid authform;
+	Relation	pg_auth_password_rel;
 
 	rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	dsc = RelationGetDescr(rel);
@@ -1301,28 +1423,38 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_password_roleid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(roleid));
+
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
-	if (HeapTupleIsValid(passtuple))
-		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
-								Anum_pg_auth_password_password, &isnull);
+	sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								true, NULL, 1, &scankey);
 
-	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+	while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
 	{
-		Relation	pg_auth_password_rel;
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+							Anum_pg_auth_password_password, &isnull);
 
-		/* MD5 uses the username as salt, so just clear it on a rename */
-		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+		{
 
-		if (HeapTupleIsValid(passtuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
-			ReleaseSysCache(passtuple);
+			/* MD5 uses the username as salt, so just clear it on a rename */
+
+			if (HeapTupleIsValid(passtuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+				ereport(NOTICE,
+					(errmsg("MD5 password cleared because of role rename")));
+
+			}
 		}
-		table_close(pg_auth_password_rel, NoLock);
-		ereport(NOTICE,
-				(errmsg("MD5 password cleared because of role rename")));
 	}
 
+	systable_endscan(sscan);
+	table_close(pg_auth_password_rel, NoLock);
+
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
 
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 805b3695b78..652c18fa94a 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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,
 			  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);
 
 			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 795f1cba555..95db4aaf958 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,13 @@ typedef enum
 	SCRAM_AUTH_FINISHED
 } scram_state_enum;
 
+typedef struct
+{
+	uint8		StoredKey[SCRAM_KEY_LEN];
+	uint8		ServerKey[SCRAM_KEY_LEN];
+} scram_secret;
+
+
 typedef struct
 {
 	scram_state_enum state;
@@ -141,10 +148,16 @@ typedef struct
 	Port	   *port;
 	bool		channel_binding_in_use;
 
+	/* 
+	 * 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_KEY_LEN];
-	uint8		ServerKey[SCRAM_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;
@@ -220,17 +233,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_auth_password.password.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
  * 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;
@@ -260,46 +276,48 @@ 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->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->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 */
+							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;
 	}
 
 	/*
@@ -310,8 +328,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 */
 	if (!got_secret)
 	{
+		state->secrets = palloc0(sizeof(scram_secret));
+
 		mock_scram_secret(state->port->user_name, &state->iterations,
-						  &state->salt, state->StoredKey, state->ServerKey);
+						  &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey);
 		state->doomed = true;
 	}
 
@@ -459,7 +479,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -476,12 +496,14 @@ 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)
+		pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN);
+	
 	result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
 								SCRAM_DEFAULT_ITERATIONS, password,
 								&errstr);
@@ -1114,47 +1136,57 @@ verify_client_proof(scram_state *state)
 	uint8		ClientSignature[SCRAM_KEY_LEN];
 	uint8		ClientKey[SCRAM_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_KEY_LEN];
-	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
-	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, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 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(PG_SHA256);
+		elog(LOG, "Trying to verify password %d", j);
+
+		if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 0)
+		{
+			elog(LOG, "could not calculate client signature");
+			pg_hmac_free(ctx);
+			continue;
+		}
 
-	pg_hmac_free(ctx);
+		elog(LOG, "success on %d", j);
 
-	/* Extract the ClientKey that the client calculated from the proof */
-	for (i = 0; i < SCRAM_KEY_LEN; 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, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
-		elog(ERROR, "could not hash stored key: %s", errstr);
+		for (i = 0; i < SCRAM_KEY_LEN; i++)
+			ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
-	if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
-		return false;
+		/* Hash it one more time, and compare with StoredKey */
+		if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
+			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	return true;
+		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) {
+			elog(LOG, "Moving forward with Password %d", j);
+			state->chosen_secret = j;
+			return true; 
+		}
+	}
+
+	return false;
 }
 
 /*
@@ -1380,7 +1412,7 @@ build_server_final_message(scram_state *state)
 	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
 
 	/* calculate ServerSignature */
-	if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 ||
+	if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 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 5cfde1eaa13..5569599ab31 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -58,8 +58,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, const char **logdetail);
 
 
 /*----------------------------------------------------------------
@@ -790,8 +789,9 @@ static int
 CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
-	int			result;
-	char	   *shadow_pass;
+	int			result = STATUS_ERROR;
+	int			i, num;
+	char	   **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -799,17 +799,21 @@ 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)
-	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
-	}
-	else
-		result = STATUS_ERROR;
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
+	if (passwords != NULL) {
+		for (i = 0; i < num; 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; passwords[i] != NULL; i++) 
+			pfree(passwords[i]);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		pfree(passwords);
+	}
+	
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -824,29 +828,15 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 static int
 CheckPWChallengeAuth(Port *port, const char **logdetail)
 {
-	int			auth_result;
-	char	   *shadow_pass;
-	PasswordType pwtype;
+	int			auth_result = STATUS_ERROR;
+	int			i, num;
+	char	  **passwords;
 
 	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);
-
-	/*
-	 * 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
-	 * 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)
-		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
 
 	/*
 	 * If 'md5' authentication is allowed, decide whether to perform 'md5' or
@@ -858,23 +848,17 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 	 * 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);
-	else
-		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
-									logdetail);
+	if (passwords != NULL) {
+		if (port->hba->auth_method == uaMD5)
+			auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
+											logdetail);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		for (i = 0; i < num; i++) 
+			pfree(passwords[i]);
 
-	/*
-	 * If get_role_password() returned error, return error, even if the
-	 * authentication succeeded.
-	 */
-	if (!shadow_pass)
-	{
-		Assert(auth_result != STATUS_OK);
-		return STATUS_ERROR;
+		pfree(passwords);
 	}
 
 	if (auth_result == STATUS_OK)
@@ -884,11 +868,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, const char **logdetail)
 {
 	char		md5Salt[4];		/* Password salt */
 	char	   *passwd;
 	int			result;
+	int			i;
 
 	if (Db_user_namespace)
 		ereport(FATAL,
@@ -909,12 +894,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; 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 745e61034c2..46b458f7d8a 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -23,6 +23,7 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
@@ -34,13 +35,15 @@
  * 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)
 {
-	HeapTuple	roleTup, passTup;
+	HeapTuple	roleTup;
 	Datum		datum;
 	bool		isnull;
-	char	   *shadow_pass;
+	char	   **passwords;
+	CatCList   *passlist;
+	int		    i;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,30 +55,33 @@ get_role_password(const char *role, const char **logdetail)
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
-	
-	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+	ReleaseSysCache(roleTup);
+	/* Find any existing password that is not the one being updated to get the salt */
+	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+	*num = passlist->n_members;
 
-	if (!HeapTupleIsValid(passTup))
+	if (passlist->n_members == 0)
 	{
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
+		ReleaseCatCacheList(passlist);
 		return NULL;			/* user has no password */
 	}
-	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
-							Anum_pg_auth_password_password, &isnull);
 
-	ReleaseSysCache(roleTup);
-	if (isnull) /* this should not happen any more but just in case */
+	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+
+	for (i = 0; i < passlist->n_members; i++)
 	{
-		ReleaseSysCache(passTup);
-		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
-							  role);
-		return NULL;			/* user has no password */
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_password, &isnull);
+		passwords[i] = TextDatumGetCString(datum);
 	}
-	shadow_pass = TextDatumGetCString(datum);
-	ReleaseSysCache(passTup);
 
-	return shadow_pass;
+	ReleaseCatCacheList(passlist);
+
+	return passwords;
 }
 
 /*
@@ -107,7 +113,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);
@@ -128,13 +134,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);
+				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'");
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a03b33b53bd..2a288ff13f9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -716,7 +716,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
+	PARALLEL PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -1132,6 +1132,15 @@ AlterOptRoleElem:
 							 errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
 							 parser_errposition(@1)));
 				}
+			| PASSNAME Sconst
+				{
+					$$ = makeDefElem("passname",
+									 (Node *)makeString($2), @1);
+				}
+			| VALID FOR Sconst
+				{
+					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+				}
 			| INHERIT
 				{
 					$$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
@@ -15855,6 +15864,7 @@ unreserved_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLANS
 			| POLICY
@@ -16433,6 +16443,7 @@ bare_label_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLACING
 			| PLANS
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 356e9cde6fb..c421ac0b19f 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHPASSWORD:
+		case AUTHPASSWORDNAME:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 49d49f13252..2c39cfb083f 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -255,12 +255,12 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
-	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+	{AuthPasswordRelationId,			/* AUTHPASSWORDNAME */
 		AuthPasswordRoleOidIndexId,
-		1,
+		2,
 		{
 			Anum_pg_auth_password_roleid,
-			0,
+			Anum_pg_auth_password_name,
 			0,
 			0
 		},
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index beaa2d40b90..4181caad30b 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -24,11 +24,13 @@
  *		typedef struct FormData_pg_auth_password
  * ----------------
  */
-CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
 {
 	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+    NameData    name;            /* name of password for multiple password support */
+
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
-    text            password;        /* password */
+    text            password BKI_FORCE_NOT_NULL;        /* password */
 #endif
 } FormData_pg_auth_password;
 
@@ -44,7 +46,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176);
 
 typedef FormData_pg_auth_password *Form_pg_auth_password;
 
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
-
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops));
 
 #endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index ce39db5a491..7223b1fdae4 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
-extern bool is_role_valid(const char *rolename, char **logdetail);
+extern bool is_role_valid(const char *rolename, const char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index b8ff8ccb417..84c9c4b4f48 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 71cc0dc2514..9892f9aad38 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, const char **logdetail);
 
 #endif							/* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e60992a0d2d..8cc6f492eeb 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,7 @@
 extern 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, char **salt,
 							   uint8 *stored_key, uint8 *server_key);
 extern bool scram_verify_plain_password(const char *username,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bcef7eed2f3..e7239b34885 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -311,6 +311,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index ab846e8b7fa..869cc4e4e20 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,7 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
-	AUTHPASSWORD,
+	AUTHPASSWORDNAME,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 4ffc41a5455..c6a84f86a84 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 -- should not contain the original salt.
 SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
-- 
2.31.1

0003-Per-password-expiration.patchapplication/octet-stream; name=0003-Per-password-expiration.patchDownload
From 83cad16a61589a434e8d97a08e6d302048627775 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Wed, 2 Mar 2022 06:07:40 -0800
Subject: [PATCH 3/3] Per-password expiration

To build on the multi-password support this
adds per-password expiration, either passed in via ALTER ROLE/CREATE
ROLE with the grammar EXPIRES IN, or via a system-wide setting
called password_valid_duration

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 121 ++++++++++++++++++++++++-
 src/backend/commands/variable.c        | 105 +++++++++++++++++++++
 src/backend/libpq/auth.c               |   9 +-
 src/backend/libpq/crypt.c              |  37 +++++++-
 src/backend/parser/gram.y              |  10 +-
 src/backend/utils/misc/guc.c           |  13 +++
 src/include/catalog/pg_auth_password.h |   1 +
 src/include/commands/user.h            |   3 +
 src/include/commands/variable.h        |   4 +
 src/include/parser/kwlist.h            |   1 +
 10 files changed, 291 insertions(+), 13 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 69daabd7928..bbb33aaa7ef 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -48,8 +48,9 @@
 Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 
 
-/* GUC parameter */
+/* GUC parameters */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+Interval *default_password_duration = NULL;
 
 /* default password name */
 const char* default_passname = "__def__";
@@ -167,6 +168,84 @@ validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
 	return true;
 }
 
+static
+Datum
+expires_in_datum(DefElem *passExpiresIn)
+{
+	Interval *interval;
+	Node	   *arg;
+	A_Const    *con;
+	TimestampTz now = GetCurrentTimestamp();
+	Datum		passExpiresIn_datum;
+	char *dateout;
+
+	if (default_password_duration != NULL) 
+	{
+		/* The default duration GUC is set, use it if nothing came from SQL
+		 * or if something came from SQL, reject it if not from a superuser
+		 */
+
+		if (passExpiresIn != NULL)
+			if (!superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("must be superuser to override password_validity_duration GUC")));
+			else
+				goto bypass;
+		else
+		{
+			passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										 PointerGetDatum(default_password_duration));
+
+			dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+			ereport(NOTICE,
+				(errmsg("Password will expire at: \"%s\" (from GUC)", dateout)));
+
+			return passExpiresIn_datum;
+		}
+	}
+
+	if (passExpiresIn == NULL) 	/* No duration requested via SQL and no system default, no expiration */
+		return PointerGetDatum(NULL);
+
+bypass:
+ 	arg = (Node *)passExpiresIn->arg;
+	if (IsA(arg, TypeCast))
+	{
+		TypeCast   *tc = (TypeCast *) passExpiresIn->arg;
+		arg = tc->arg;
+	}
+
+	if (!IsA(arg, A_Const))
+	{
+		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
+		return PointerGetDatum(NULL);
+	}
+	con = (A_Const *) arg;
+
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+								CStringGetDatum(strVal(&con->val)),
+								ObjectIdGetDatum(InvalidOid),
+								Int32GetDatum(-1)));
+
+	passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										PointerGetDatum(interval));
+
+	dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+	ereport(NOTICE,
+		(errmsg("Password will expire at: \"%s\" (from SQL)", dateout)));
+
+	return passExpiresIn_datum;
+}
+
 /*
  * CREATE ROLE
  */
@@ -211,7 +290,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
-
+	DefElem    *dpassExpiresIn = NULL;
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -320,6 +399,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 
 		else
 			elog(ERROR, "option \"%s\" not recognized",
@@ -508,6 +593,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
 		HeapTuple	new_tuple;
+		Datum		passExpiresIn_datum;
 
 		/*
 		 * Don't allow an empty password. Libpq treats an empty password the
@@ -529,18 +615,26 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+			if (passExpiresIn_datum != PointerGetDatum(NULL))
+				new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+			else
+				new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+	
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, stmt->role,
 										   password);
 
-			MemSet(new_password_record, 0, sizeof(new_password_record));
-			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 			if (passname != NULL)
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(passname));
 			else
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -647,6 +741,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
 	bool		validUntil_null;
+	Datum		passExpiresIn_datum; /* Time period until password expires */
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
@@ -659,6 +754,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
+	DefElem	   *dpassExpiresIn = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -742,6 +838,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -762,6 +864,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (dpassName)
 		passname = strVal(dpassName->arg);
 
+
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
 	 */
@@ -848,6 +951,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
 
+	passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+	if (passExpiresIn_datum != PointerGetDatum(NULL)) {
+		new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+		new_password_record_repl[Anum_pg_auth_password_expiration - 1] = true;
+	}
+	else
+		new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
 
 	/*
 	 * issuper/createrole/etc
@@ -957,7 +1067,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
-	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true 
+		|| new_password_record_repl[Anum_pg_auth_password_expiration - 1] == true)
 	{
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4a..ca85a10b80b 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "commands/user.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -933,3 +934,107 @@ show_role(void)
 	/* Otherwise we can just use the GUC string */
 	return role_string ? role_string : "none";
 }
+
+
+/*
+ * check_password_duration: GUC check_hook for password_duration
+ */
+bool
+check_password_duration(char **newval, void **extra, GucSource source)
+{
+	Interval	*new_interval;
+	char	   *endptr;
+
+	const char *valueptr = *newval;
+	char	   *val;
+	Interval   *interval;
+
+	if (newval == NULL || *newval == NULL) {
+		extra = NULL;
+		return true;
+	}
+
+	elog(NOTICE,"Setting password duration to \"%s\"",
+					*newval);
+
+	while (isspace((unsigned char) *valueptr))
+		valueptr++;
+	if (*valueptr != '\'') {
+		val = pstrdup(valueptr);
+	}
+	else
+	{
+		valueptr++;
+		val = pstrdup(valueptr);
+		/* Check and remove trailing quote */
+		endptr = strchr(val, '\'');
+		if (!endptr || endptr[1] != '\0')
+		{
+			pfree(val);
+			return false;
+		}
+		*endptr = '\0';
+	}
+
+	/*
+		* Try to parse it.  XXX an invalid interval format will result in
+		* ereport(ERROR), which is not desirable for GUC.  We did what we
+		* could to guard against this in flatten_set_variable_args, but a
+		* string coming in from postgresql.conf might contain anything.
+		*/
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+														CStringGetDatum(val),
+														ObjectIdGetDatum(InvalidOid),
+														Int32GetDatum(-1)));
+
+	pfree(val);
+
+	if (!interval) {
+		return false;
+	}
+
+	new_interval = malloc(sizeof(Interval));
+	memcpy(new_interval, interval, sizeof(Interval));
+	pfree(interval);
+
+	/*
+	 * Pass back data for assign_password_validity to use
+	 */
+	*extra = malloc(sizeof(Interval *));
+	if (!*extra)
+		return false;
+	*((Interval **) *extra) = new_interval;
+
+	return true;
+}
+
+/*
+ * assign_password_validity: GUC assign_hook for timezone
+ */
+void
+assign_password_duration(const char *newval, void *extra)
+{
+	if (extra == NULL)
+		default_password_duration = NULL;
+	else
+		default_password_duration = *((Interval **) extra);
+}
+
+/*
+ * show_password_validity: GUC show_hook for timezone
+ */
+const char *
+show_password_duration(void)
+{
+	const char *intervalout;
+	if (default_password_duration == NULL) {
+		return "";
+	}
+	intervalout = DatumGetCString(DirectFunctionCall1(interval_out,
+										PointerGetDatum(default_password_duration)));
+
+	if (intervalout != NULL)
+		return intervalout;
+
+	return "";
+}
\ No newline at end of file
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5569599ab31..641cc92549b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -855,9 +855,14 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
 											logdetail);
 
-		for (i = 0; i < num; i++) 
-			pfree(passwords[i]);
+		for (i = 0; i < num; i++) {
 
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(LOG,
+					(errmsg("Password %d was null", i)));
+		}
 		pfree(passwords);
 	}
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 46b458f7d8a..877bb7a4f31 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -40,10 +40,12 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
+	TimestampTz current, vuntil = 0;
+
 	bool		isnull;
 	char	   **passwords;
 	CatCList   *passlist;
-	int		    i;
+	int		    i, j = 0, num_valid_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -58,7 +60,6 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	ReleaseSysCache(roleTup);
 	/* Find any existing password that is not the one being updated to get the salt */
 	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
-	*num = passlist->n_members;
 
 	if (passlist->n_members == 0)
 	{
@@ -68,15 +69,41 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 		return NULL;			/* user has no password */
 	}
 
-	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+	current = GetCurrentTimestamp();
+
+	for (i = 0; i < passlist->n_members; i++)
+	{
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+			num_valid_passwords++;
+	}
 
+	passwords = palloc0(sizeof(char *) * num_valid_passwords); 
+	*num = num_valid_passwords;
+	
 	for (i = 0; i < passlist->n_members; i++)
 	{
 		HeapTuple	tup = &passlist->members[i]->tuple;
 
 		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
-							Anum_pg_auth_password_password, &isnull);
-		passwords[i] = TextDatumGetCString(datum);
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+		{
+			datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+								Anum_pg_auth_password_password, &isnull);
+			passwords[j++] = pstrdup(TextDatumGetCString(datum));
+		}
 	}
 
 	ReleaseCatCacheList(passlist);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a288ff13f9..8e0887618d0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -682,7 +682,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DOUBLE_P DROP
 
 	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPIRES EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -1137,6 +1137,12 @@ AlterOptRoleElem:
 					$$ = makeDefElem("passname",
 									 (Node *)makeString($2), @1);
 				}
+			| EXPIRES IN_P Sconst opt_interval
+				{
+					TypeName *t = SystemTypeName("interval");
+					t->typmods = $4;
+					$$ = makeDefElem("expiresin", (Node *)makeStringConstCast($3, @3, t), @1);
+				}
 			| VALID FOR Sconst
 				{
 					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
@@ -15766,6 +15772,7 @@ unreserved_keyword:
 			| EXCLUDING
 			| EXCLUSIVE
 			| EXECUTE
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -16311,6 +16318,7 @@ bare_label_keyword:
 			| EXCLUSIVE
 			| EXECUTE
 			| EXISTS
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1e3650184b1..a1295a3070f 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -682,6 +682,7 @@ static char *recovery_target_string;
 static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
+static char *password_duration_string;
 
 
 /* should be static, but commands/variable.c needs to get at this */
@@ -4662,6 +4663,17 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"password_valid_duration", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Specifies the default validity duration of new passwords."),
+			NULL,
+			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
+		},
+		&password_duration_string,
+		NULL,
+		check_password_duration, assign_password_duration, show_password_duration
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
@@ -5039,6 +5051,7 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index 4181caad30b..1cb0a211ee7 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -31,6 +31,7 @@ CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_RO
 
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
     text            password BKI_FORCE_NOT_NULL;        /* password */
+    timestamptz     expiration BKI_FORCE_NULL;	        /* password expiration time, if any */
 #endif
 } FormData_pg_auth_password;
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 7223b1fdae4..1203358933a 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -19,6 +19,9 @@
 /* GUC. Is actually of type PasswordType. */
 extern int	Password_encryption;
 
+/* GUC. system-wide password validity duration */
+extern Interval *default_password_duration;
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/commands/variable.h b/src/include/commands/variable.h
index 0e5ddcbcf37..3b941c554cb 100644
--- a/src/include/commands/variable.h
+++ b/src/include/commands/variable.h
@@ -34,5 +34,9 @@ extern void assign_session_authorization(const char *newval, void *extra);
 extern bool check_role(char **newval, void **extra, GucSource source);
 extern void assign_role(const char *newval, void *extra);
 extern const char *show_role(void);
+extern bool check_password_duration(char **newval, void **extra, GucSource source);
+extern void assign_password_duration(const char *newval, void *extra);
+extern const char *show_password_duration(void);
+
 
 #endif							/* VARIABLE_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index e7239b34885..0e1c8098363 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -159,6 +159,7 @@ PG_KEYWORD("excluding", EXCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exclusive", EXCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("execute", EXECUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exists", EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("expires", EXPIRES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("explain", EXPLAIN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("expression", EXPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("extension", EXTENSION, UNRESERVED_KEYWORD, BARE_LABEL)
-- 
2.31.1

0001-Move-rolpassword-out-of-pg_authid-into-a-new-table.patchapplication/octet-stream; name=0001-Move-rolpassword-out-of-pg_authid-into-a-new-table.patchDownload
From 839c2fe3d8553455b842ea25c2ea24b11596cde6 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Thu, 9 Dec 2021 16:18:25 -0800
Subject: [PATCH 1/3] Move rolpassword out of pg_authid into a new table

In preparation for more flexibility in password
management the rolpassword column needs to be
moved into a new table.

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/catalog/Makefile                 |   2 +-
 src/backend/catalog/catalog.c                |   7 +-
 src/backend/catalog/system_views.sql         |   5 +-
 src/backend/commands/user.c                  | 226 ++++++++++++----
 src/backend/libpq/auth-sasl.c                |   2 +-
 src/backend/libpq/auth-scram.c               |   4 +-
 src/backend/libpq/auth.c                     |  16 ++
 src/backend/libpq/crypt.c                    |  40 ++-
 src/backend/utils/cache/catcache.c           |   1 +
 src/backend/utils/cache/relcache.c           |  13 +-
 src/backend/utils/cache/syscache.c           |  12 +
 src/bin/initdb/initdb.c                      |   2 +-
 src/bin/pg_dump/pg_dumpall.c                 |  13 +-
 src/common/scram-common.c                    |   2 +-
 src/include/catalog/pg_auth_password.h       |  50 ++++
 src/include/catalog/pg_authid.dat            |  26 +-
 src/include/catalog/pg_authid.h              |   9 +-
 src/include/commands/user.h                  |   1 +
 src/include/libpq/crypt.h                    |   2 +-
 src/include/utils/syscache.h                 |   1 +
 src/test/regress/expected/create_index.out   |  14 +-
 src/test/regress/expected/oidjoins.out       |   1 +
 src/test/regress/expected/password.out       |  44 ++--
 src/test/regress/expected/roleattributes.out | 256 +++++++++----------
 src/test/regress/expected/rules.out          |   5 +-
 src/test/regress/expected/tablespace.out     |  12 +-
 src/test/regress/sql/create_index.sql        |  10 +-
 src/test/regress/sql/password.sql            |  32 ++-
 src/test/regress/sql/roleattributes.sql      |  64 ++---
 src/test/regress/sql/tablespace.sql          |   8 +-
 30 files changed, 562 insertions(+), 318 deletions(-)
 create mode 100644 src/include/catalog/pg_auth_password.h

diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index eefebb7bb83..ea6d7e47340 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -61,7 +61,7 @@ CATALOG_HEADERS := \
 	pg_rewrite.h pg_trigger.h pg_event_trigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h \
-	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
+	pg_authid.h pg_auth_members.h pg_auth_password.h pg_shdepend.h pg_shdescription.h \
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index dfd5fb669ee..410b3328011 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -28,6 +28,7 @@
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
@@ -245,6 +246,7 @@ IsSharedRelation(Oid relationId)
 {
 	/* These are the shared catalogs (look for BKI_SHARED_RELATION) */
 	if (relationId == AuthIdRelationId ||
+		relationId == AuthPasswordRelationId ||
 		relationId == AuthMemRelationId ||
 		relationId == DatabaseRelationId ||
 		relationId == SharedDescriptionRelationId ||
@@ -258,6 +260,7 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdRolnameIndexId ||
 		relationId == AuthIdOidIndexId ||
+		relationId == AuthPasswordRoleOidIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == DatabaseNameIndexId ||
@@ -275,8 +278,8 @@ IsSharedRelation(Oid relationId)
 		relationId == SubscriptionNameIndexId)
 		return true;
 	/* These are their toast tables and toast indexes */
-	if (relationId == PgAuthidToastTable ||
-		relationId == PgAuthidToastIndex ||
+	if (relationId == PgAuthPasswordToastTable ||
+		relationId == PgAuthPasswordToastIndex ||
 		relationId == PgDatabaseToastTable ||
 		relationId == PgDatabaseToastIndex ||
 		relationId == PgDbRoleSettingToastTable ||
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87b..0f2ba76bef9 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -40,11 +40,14 @@ CREATE VIEW pg_shadow AS
         rolsuper AS usesuper,
         rolreplication AS userepl,
         rolbypassrls AS usebypassrls,
-        rolpassword AS passwd,
+        p.password AS passwd,
         rolvaliduntil AS valuntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
+    LEFT JOIN pg_auth_password p
+    ON p.roleid = pg_authid.oid
+
     WHERE rolcanlogin;
 
 REVOKE ALL ON pg_shadow FROM public;
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246bb..9ff99342eef 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -23,6 +23,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "commands/comment.h"
@@ -64,6 +65,42 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+/*
+ * Is the role able to log in
+*/
+bool
+is_role_valid(const char *rolename, char **logdetail)
+{
+	HeapTuple	roleTup;
+	Datum		datum;
+	bool		isnull;
+	TimestampTz vuntil = 0;
+
+	/* Get role info from pg_authid */
+	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(rolename));
+	if (!HeapTupleIsValid(roleTup))
+	{
+		*logdetail = psprintf(_("Role \"%s\" does not exist."),
+							  rolename);
+		return false;			/* no such user */
+	}
+
+	datum = SysCacheGetAttr(AUTHNAME, roleTup,
+							Anum_pg_authid_rolvaliduntil, &isnull);
+	ReleaseSysCache(roleTup);
+
+	if (!isnull)
+		vuntil = DatumGetTimestampTz(datum);
+
+	if (!isnull && vuntil < GetCurrentTimestamp())
+	{
+		*logdetail = psprintf(_("User \"%s\" has an expired password."), rolename);
+		return false;
+	}
+
+	return true;
+}
+
 
 /*
  * CREATE ROLE
@@ -351,43 +388,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
 	new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
 	new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
-
-	if (password)
-	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
-
-		/*
-		 * Don't allow an empty password. Libpq treats an empty password the
-		 * same as no password at all, and won't even try to authenticate. But
-		 * other clients might, so allowing it would be confusing. By clearing
-		 * the password when an empty string is specified, the account is
-		 * consistently locked for all clients.
-		 *
-		 * Note that this only covers passwords stored in the database itself.
-		 * There are also checks in the authentication code, to forbid an
-		 * empty password from being used with authentication methods that
-		 * fetch the password from an external system, like LDAP or PAM.
-		 */
-		if (password[0] == '\0' ||
-			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
-		{
-			ereport(NOTICE,
-					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-		}
-		else
-		{
-			/* 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);
-		}
-	}
-	else
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
@@ -422,6 +422,60 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	CatalogTupleInsert(pg_authid_rel, tuple);
 
+	if (password)
+	{
+		char	   *shadow_pass;
+		char	   *logdetail;
+		Datum		new_password_record[Natts_pg_auth_password];
+		bool		new_password_record_nulls[Natts_pg_auth_password];
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple	new_tuple;
+
+		/*
+		 * Don't allow an empty password. Libpq treats an empty password the
+		 * same as no password at all, and won't even try to authenticate. But
+		 * other clients might, so allowing it would be confusing. By clearing
+		 * the password when an empty string is specified, the account is
+		 * consistently locked for all clients.
+		 *
+		 * Note that this only covers passwords stored in the database itself.
+		 * There are also checks in the authentication code, to forbid an
+		 * empty password from being used with authentication methods that
+		 * fetch the password from an external system, like LDAP or PAM.
+		 */
+		if (password[0] == '\0' ||
+			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+		}
+		else
+		{
+			/* Encrypt the password to the requested format. */
+			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+										   password);
+
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			/* open password table and insert it. */
+			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+
+			new_password_record[Anum_pg_auth_password_password - 1] =
+				CStringGetTextDatum(shadow_pass);
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+			table_close(pg_auth_password_rel, NoLock);
+
+		}
+	}
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -495,6 +549,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	Datum		new_record[Natts_pg_authid];
 	bool		new_record_nulls[Natts_pg_authid];
 	bool		new_record_repl[Natts_pg_authid];
+	Datum		new_password_record[Natts_pg_auth_password];
+	bool		new_password_record_nulls[Natts_pg_auth_password];
+	bool		new_password_record_repl[Natts_pg_auth_password];
 	Relation	pg_authid_rel;
 	TupleDesc	pg_authid_dsc;
 	HeapTuple	tuple,
@@ -624,6 +681,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	rolename = pstrdup(NameStr(authform->rolname));
 	roleid = authform->oid;
 
+
 	/*
 	 * To mess with a superuser or replication role in any way you gotta be
 	 * superuser.  We also insist on superuser to change the BYPASSRLS
@@ -694,6 +752,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_record, 0, sizeof(new_record));
 	MemSet(new_record_nulls, false, sizeof(new_record_nulls));
 	MemSet(new_record_repl, false, sizeof(new_record_repl));
+	MemSet(new_password_record, 0, sizeof(new_password_record));
+	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
+
 
 	/*
 	 * issuper/createrole/etc
@@ -752,24 +814,24 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		{
 			ereport(NOTICE,
 					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+			new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 		}
 		else
 		{
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, rolename,
 										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
+			new_password_record[Anum_pg_auth_password_password - 1] =
 				CStringGetTextDatum(shadow_pass);
 		}
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
 	if (dpassword && dpassword->arg == NULL)
 	{
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+		new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* valid until */
@@ -786,12 +848,48 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
 								  new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
-
-	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
-
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	{
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple   password_tuple;
+
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+
+		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
+		{
+			if (HeapTupleIsValid(password_tuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &password_tuple->t_self);
+				ReleaseSysCache(password_tuple);
+			}
+		}
+		else if (HeapTupleIsValid(password_tuple)) /* update existing password */
+		{
+			new_tuple = heap_modify_tuple(password_tuple, pg_auth_password_dsc, new_password_record,
+										new_password_record_nulls, new_password_record_repl);
+			CatalogTupleUpdate(pg_auth_password_rel, &password_tuple->t_self, new_tuple);
+			ReleaseSysCache(password_tuple);
+			heap_freetuple(new_tuple);
+		}
+		else /* insert new password */
+		{
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+		}
+
+		table_close(pg_auth_password_rel, NoLock);
+	}
+	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -901,7 +999,6 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 	return roleid;
 }
 
-
 /*
  * DROP ROLE
  */
@@ -909,7 +1006,8 @@ void
 DropRole(DropRoleStmt *stmt)
 {
 	Relation	pg_authid_rel,
-				pg_auth_members_rel;
+				pg_auth_members_rel,
+				pg_auth_password_rel;
 	ListCell   *item;
 
 	if (!have_createrole_privilege())
@@ -923,6 +1021,8 @@ DropRole(DropRoleStmt *stmt)
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+
 
 	foreach(item, stmt->roles)
 	{
@@ -1056,6 +1156,15 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedComments(roleid, AuthIdRelationId);
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
+		/*
+		 * Drop password
+		 */
+		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
+		if (HeapTupleIsValid(tuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
+			ReleaseSysCache(tuple);
+		}
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1077,7 +1186,9 @@ DropRole(DropRoleStmt *stmt)
 	 * Now we can clean up; but keep locks until commit.
 	 */
 	table_close(pg_auth_members_rel, NoLock);
+	table_close(pg_auth_password_rel, NoLock);
 	table_close(pg_authid_rel, NoLock);
+
 }
 
 /*
@@ -1087,11 +1198,12 @@ ObjectAddress
 RenameRole(const char *oldname, const char *newname)
 {
 	HeapTuple	oldtuple,
-				newtuple;
+				newtuple,
+				passtuple;
 	TupleDesc	dsc;
 	Relation	rel;
 	Datum		datum;
-	bool		isnull;
+	bool		isnull = true;
 	Datum		repl_val[Natts_pg_authid];
 	bool		repl_null[Natts_pg_authid];
 	bool		repl_repl[Natts_pg_authid];
@@ -1189,14 +1301,24 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
+	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+
+	if (HeapTupleIsValid(passtuple))
+		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
+								Anum_pg_auth_password_password, &isnull);
 
 	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
 	{
+		Relation	pg_auth_password_rel;
+
 		/* MD5 uses the username as salt, so just clear it on a rename */
-		repl_repl[Anum_pg_authid_rolpassword - 1] = true;
-		repl_null[Anum_pg_authid_rolpassword - 1] = true;
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
+		if (HeapTupleIsValid(passtuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+			ReleaseSysCache(passtuple);
+		}
+		table_close(pg_auth_password_rel, NoLock);
 		ereport(NOTICE,
 				(errmsg("MD5 password cleared because of role rename")));
 	}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index a1d7dbb6d58..805b3695b78 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -33,7 +33,7 @@
  * implementation.
  *
  * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
+ * authenticated, from pg_auth_password.password.  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.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218ab..795f1cba555 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -220,7 +220,7 @@ 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.
+ * 'shadow_pass' is the role's stored secret, from pg_auth_password.password.
  * 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
@@ -454,7 +454,7 @@ 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_auth_password.password.
  *
  * The result is palloc'd, so caller is responsible for freeing it.
  */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f31353..5cfde1eaa13 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -592,10 +592,26 @@ ClientAuthentication(Port *port)
 
 		case uaMD5:
 		case uaSCRAM:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPWChallengeAuth(port, &logdetail);
 			break;
 
 		case uaPassword:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPasswordAuth(port, &logdetail);
 			break;
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 1ff8b0507d4..745e61034c2 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_auth_password.password.
  *
  * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -16,6 +16,7 @@
 #include <unistd.h>
 
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "common/md5.h"
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
@@ -36,8 +37,7 @@
 char *
 get_role_password(const char *role, const char **logdetail)
 {
-	TimestampTz vuntil = 0;
-	HeapTuple	roleTup;
+	HeapTuple	roleTup, passTup;
 	Datum		datum;
 	bool		isnull;
 	char	   *shadow_pass;
@@ -51,33 +51,29 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;			/* no such user */
 	}
 
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
+	
+	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+
+	if (!HeapTupleIsValid(passTup))
 	{
-		ReleaseSysCache(roleTup);
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
-
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
-	if (!isnull)
-		vuntil = DatumGetTimestampTz(datum);
+	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
+							Anum_pg_auth_password_password, &isnull);
 
 	ReleaseSysCache(roleTup);
-
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (isnull) /* this should not happen any more but just in case */
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
+		ReleaseSysCache(passTup);
+		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
-		return NULL;
+		return NULL;			/* user has no password */
 	}
+	shadow_pass = TextDatumGetCString(datum);
+	ReleaseSysCache(passTup);
 
 	return shadow_pass;
 }
@@ -156,7 +152,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_auth_password.password.
  * '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.
  *
@@ -211,7 +207,7 @@ 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_auth_password.password.
  * '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/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index ec073e1ed06..356e9cde6fb 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,6 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
+		case AUTHPASSWORD:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce5729..71773cb992c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_namespace.h"
@@ -112,6 +113,7 @@ static const FormData_pg_attribute Desc_pg_proc[Natts_pg_proc] = {Schema_pg_proc
 static const FormData_pg_attribute Desc_pg_type[Natts_pg_type] = {Schema_pg_type};
 static const FormData_pg_attribute Desc_pg_database[Natts_pg_database] = {Schema_pg_database};
 static const FormData_pg_attribute Desc_pg_authid[Natts_pg_authid] = {Schema_pg_authid};
+static const FormData_pg_attribute Desc_pg_auth_password[Natts_pg_auth_password] = {Schema_pg_auth_password};
 static const FormData_pg_attribute Desc_pg_auth_members[Natts_pg_auth_members] = {Schema_pg_auth_members};
 static const FormData_pg_attribute Desc_pg_index[Natts_pg_index] = {Schema_pg_index};
 static const FormData_pg_attribute Desc_pg_shseclabel[Natts_pg_shseclabel] = {Schema_pg_shseclabel};
@@ -3461,6 +3463,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case DatabaseRelationId:
 		case AuthIdRelationId:
+		case AuthPasswordRelationId:
 		case AuthMemRelationId:
 		case RelationRelationId:
 		case AttributeRelationId:
@@ -3881,7 +3884,7 @@ RelationCacheInitialize(void)
  *		RelationCacheInitializePhase2
  *
  *		This is called to prepare for access to shared catalogs during startup.
- *		We must at least set up nailed reldescs for pg_database, pg_authid,
+ *		We must at least set up nailed reldescs for pg_database, pg_authid, pg_auth_password,
  *		pg_auth_members, and pg_shseclabel. Ideally we'd like to have reldescs
  *		for their indexes, too.  We attempt to load this information from the
  *		shared relcache init file.  If that's missing or broken, just make
@@ -3926,8 +3929,10 @@ RelationCacheInitializePhase2(void)
 				  Natts_pg_shseclabel, Desc_pg_shseclabel);
 		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
 				  Natts_pg_subscription, Desc_pg_subscription);
+		formrdesc("pg_auth_password", AuthPasswordRelation_Rowtype_Id, true,
+				  Natts_pg_auth_password, Desc_pg_auth_password);
 
-#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_RELS	6	/* fix if you change list above */
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -4066,8 +4071,10 @@ RelationCacheInitializePhase3(void)
 							AuthMemRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
+		load_critical_index(AuthPasswordRoleOidIndexId,
+							AuthPasswordRelationId);
 
-#define NUM_CRITICAL_SHARED_INDEXES 6	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_INDEXES 7	/* fix if you change list above */
 
 		criticalSharedRelcachesBuilt = true;
 	}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4e7819f1e2..49d49f13252 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -254,6 +255,17 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+		AuthPasswordRoleOidIndexId,
+		1,
+		{
+			Anum_pg_auth_password_roleid,
+			0,
+			0,
+			0
+		},
+		8
+	},
 	{
 		CastRelationId,			/* CASTSOURCETARGET */
 		CastSourceTargetIndexId,
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 97f15971e2b..8958e07fd30 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1444,7 +1444,7 @@ setup_auth(FILE *cmdfd)
 		 * The authid table shouldn't be readable except through views, to
 		 * ensure passwords are not publicly visible.
 		 */
-		"REVOKE ALL ON pg_authid FROM public;\n\n",
+		"REVOKE ALL ON pg_auth_password FROM public;\n\n",
 		NULL
 	};
 
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9c9f7c6d63c..eb259d2b518 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -753,7 +753,18 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* note: rolconfig is dumped later */
-	if (server_version >= 90600)
+	if (server_version >= 150000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, p.password as rolpassword, "
+						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s LEFT JOIN pg_auth_password p ON %s.oid = p.roleid "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog, role_catalog, role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index 12686259299..d9d0cba7379 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -181,7 +181,7 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result,
 
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_auth_password.password.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
new file mode 100644
index 00000000000..beaa2d40b90
--- /dev/null
+++ b/src/include/catalog/pg_auth_password.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_auth_password.h
+ *	  definition of the "authorization identifier" system catalog (pg_auth_password)
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_auth_password.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_AUTH_PASSWORD_H
+#define PG_AUTH_PASSWORD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_auth_password_d.h"
+
+/* ----------------
+ *		pg_auth_password definition.  cpp turns this into
+ *		typedef struct FormData_pg_auth_password
+ * ----------------
+ */
+CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+{
+	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+#ifdef CATALOG_VARLEN                /* variable-length fields start here */
+    text            password;        /* password */
+#endif
+} FormData_pg_auth_password;
+
+/* ----------------
+ *		Form_pg_auth_password corresponds to a pointer to a tuple with
+ *		the format of pg_auth_password relation.
+ * ----------------
+ */
+
+DECLARE_TOAST(pg_auth_password, 4175, 4176);
+#define PgAuthPasswordToastTable 4175
+#define PgAuthPasswordToastIndex 4176
+
+typedef FormData_pg_auth_password *Form_pg_auth_password;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
+
+
+#endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6c28119fa1a..da36375a582 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,66 +23,66 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_null_' },
 { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINTER',
   rolname => 'pg_checkpointer', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f9..0cb5675a33f 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -42,9 +42,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
-#ifdef CATALOG_VARLEN			/* variable-length fields start here */
-	text		rolpassword;	/* password, if any */
-	timestamptz rolvaliduntil;	/* password expiration time, if any */
+#ifdef CATALOG_VARLEN
+	timestamptz rolvaliduntil BKI_FORCE_NULL;	/* role expiration time, if any */
 #endif
 } FormData_pg_authid;
 
@@ -55,10 +54,6 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
  */
 typedef FormData_pg_authid *Form_pg_authid;
 
-DECLARE_TOAST(pg_authid, 4175, 4176);
-#define PgAuthidToastTable 4175
-#define PgAuthidToastIndex 4176
-
 DECLARE_UNIQUE_INDEX(pg_authid_rolname_index, 2676, AuthIdRolnameIndexId, on pg_authid using btree(rolname name_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_authid_oid_index, 2677, AuthIdOidIndexId, on pg_authid using btree(oid oid_ops));
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65fd..ce39db5a491 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,6 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
+extern bool is_role_valid(const char *rolename, char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 3238cf66d3a..b8ff8ccb417 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,7 +21,7 @@
  * 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
+ * in pg_auth_password.password. They are also the allowed values for the
  * password_encryption GUC.
  */
 typedef enum PasswordType
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 9c1a76e8bb6..ab846e8b7fa 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,6 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
+	AUTHPASSWORD,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d55aec3a1d0..9875b277f26 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2515,10 +2515,10 @@ REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
 ERROR:  cannot reindex system catalogs concurrently
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 ERROR:  cannot reindex system catalogs concurrently
@@ -2817,10 +2817,10 @@ ERROR:  must be owner of schema schema_to_reindex
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
-REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+REINDEX TABLE pg_toast.pg_toast_1262;
+ERROR:  must be owner of table pg_toast_1262
+REINDEX INDEX pg_toast.pg_toast_1262_index;
+ERROR:  must be owner of index pg_toast_1262_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..b69a2a72a7b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_password {roleid} => pg_authid {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 7c84c9da337..4ffc41a5455 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -24,10 +24,12 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
@@ -40,12 +42,14 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
-       rolname       | rolpassword 
----------------------+-------------
+    ORDER BY rolname, password;
+       rolname       | password 
+---------------------+----------
  regress_passwd2_new | 
 (1 row)
 
@@ -72,10 +76,12 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
@@ -95,9 +101,11 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
- rolpassword 
--------------
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
+ password 
+----------
  
 (1 row)
 
@@ -110,8 +118,10 @@ CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
          rolname         | is_rolpassword_rehashed 
@@ -134,11 +144,13 @@ DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
- rolname | rolpassword 
----------+-------------
+    ORDER BY rolname, password;
+ rolname | password 
+---------+----------
 (0 rows)
 
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173e..18a9dacadde 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -1,233 +1,233 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
-           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
-          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
-           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 -- clean up roles
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1420288d67b..6daf57abf76 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1710,11 +1710,12 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolsuper AS usesuper,
     pg_authid.rolreplication AS userepl,
     pg_authid.rolbypassrls AS usebypassrls,
-    pg_authid.rolpassword AS passwd,
+    p.password AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
     s.setconfig AS useconfig
-   FROM (pg_authid
+   FROM ((pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
+     LEFT JOIN pg_auth_password p ON ((p.roleid = pg_authid.oid)))
   WHERE pg_authid.rolcanlogin;
 pg_shmem_allocations| SELECT pg_get_shmem_allocations.name,
     pg_get_shmem_allocations.off,
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index 2dfbcfdebe1..f013c931087 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -44,13 +44,13 @@ ERROR:  cannot move system relation "pg_authid_rolname_index"
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 ERROR:  cannot reindex system catalogs concurrently
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 ERROR:  cannot reindex system catalogs concurrently
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index d8fded3d930..8d649f2355f 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -1068,9 +1068,9 @@ REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 COMMIT;
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 -- Warns about catalog relations
 REINDEX SCHEMA CONCURRENTLY pg_catalog;
@@ -1243,8 +1243,8 @@ REINDEX SCHEMA schema_to_reindex;
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-REINDEX INDEX pg_toast.pg_toast_1260_index;
+REINDEX TABLE pg_toast.pg_toast_1262;
+REINDEX INDEX pg_toast.pg_toast_1262_index;
 
 -- Clean up
 RESET ROLE;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 98f49916e5d..bb0c815121f 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -23,18 +23,22 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- Rename a role
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 
 -- Change passwords with ALTER USER. With plaintext or already-encrypted
@@ -63,16 +67,20 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
 ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
 
 -- Test with invalid stored and server keys.
 --
@@ -84,8 +92,10 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
 
@@ -103,7 +113,9 @@ DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d7303..09505c6a3be 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -1,83 +1,83 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
 
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 
 -- clean up roles
 DROP ROLE regress_test_def_superuser;
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index 896f05cea32..24208c20c3d 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -38,10 +38,10 @@ REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am;
 REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid;
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
 REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid;
-- 
2.31.1

#2Joshua Brindle
joshua.brindle@crunchydata.com
In reply to: Joshua Brindle (#1)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Wed, Mar 2, 2022 at 9:58 AM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:

This is not intended for PG15.

Attached are a proof of concept patchset to implement multiple valid
passwords, which have independent expirations, set by a GUC or SQL
using an interval.

<snip>

postgres=# select * from pg_auth_password ;
roleid | name |
password
| expiration
--------+---------+-------------------------------------------------------------------------------------------------------------------
--------------------+-------------------------------
10 | __def__ |
SCRAM-SHA-256$4096:yGiHIYPwc2az7xj/7TIyTA==$OQL/AEcEY1yOCNbrZEj4zDvNnOLpIqltOW1uQvosLvc=:9VRRppuIkSrwhiBN5ePy8wB1y
zDa/2uX0WUx6gXi93E= |
16384 | __def__ |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$1Ivp4d+vAWxowpuGEn05KR9lxyGOms3yy85k3D7XpBg=:k8xUjU6xrJG17PMGa/Zya6pAE
/M7pEDaoIFmWvNIEUg= | 2022-03-02 06:52:31.217193-08
16384 | newone |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$WK3+41CCGDognSnZrtpHhv00z9LuVUjHR1hWq8T1+iE=:w2C5GuhgiEB7wXqPxYfxBKB+e
hm4h6Oeif1uzpPIFVk= | 2022-03-03 06:47:53.728159-08
(3 rows)

There's obviously a salt problem here that I'll need to fix that
apparently snuck in at the last rebase, but this brings up one aspect
of the patchset I didn't mention in the original email:

For the SCRAM protocol to work as is with existing clients the salt
for each password must be the same. Right now ALTER USER will find and
reuse the salt, but a user passing in a pre-computed SCRAM secret
currently has no way to get the salt.

for \password (we'll need a new one that takes a password name) I was
thinking libpq could hold onto the salt that was used to log in, but
for outside computation we'll need some way for the client to request
it.

None of that is done yet.

#3Joshua Brindle
joshua.brindle@crunchydata.com
In reply to: Joshua Brindle (#2)
3 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Wed, Mar 2, 2022 at 10:35 AM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:

On Wed, Mar 2, 2022 at 9:58 AM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:

This is not intended for PG15.

Attached are a proof of concept patchset to implement multiple valid
passwords, which have independent expirations, set by a GUC or SQL
using an interval.

<snip>

postgres=# select * from pg_auth_password ;
roleid | name |
password
| expiration
--------+---------+-------------------------------------------------------------------------------------------------------------------
--------------------+-------------------------------
10 | __def__ |
SCRAM-SHA-256$4096:yGiHIYPwc2az7xj/7TIyTA==$OQL/AEcEY1yOCNbrZEj4zDvNnOLpIqltOW1uQvosLvc=:9VRRppuIkSrwhiBN5ePy8wB1y
zDa/2uX0WUx6gXi93E= |
16384 | __def__ |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$1Ivp4d+vAWxowpuGEn05KR9lxyGOms3yy85k3D7XpBg=:k8xUjU6xrJG17PMGa/Zya6pAE
/M7pEDaoIFmWvNIEUg= | 2022-03-02 06:52:31.217193-08
16384 | newone |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$WK3+41CCGDognSnZrtpHhv00z9LuVUjHR1hWq8T1+iE=:w2C5GuhgiEB7wXqPxYfxBKB+e
hm4h6Oeif1uzpPIFVk= | 2022-03-03 06:47:53.728159-08
(3 rows)

There's obviously a salt problem here that I'll need to fix that
apparently snuck in at the last rebase, but this brings up one aspect
of the patchset I didn't mention in the original email:

Attached are fixed patches rebased against the lastest master.

Show quoted text

For the SCRAM protocol to work as is with existing clients the salt
for each password must be the same. Right now ALTER USER will find and
reuse the salt, but a user passing in a pre-computed SCRAM secret
currently has no way to get the salt.

for \password (we'll need a new one that takes a password name) I was
thinking libpq could hold onto the salt that was used to log in, but
for outside computation we'll need some way for the client to request
it.

None of that is done yet.

Attachments:

v1-0003-Per-password-expiration.patchapplication/octet-stream; name=v1-0003-Per-password-expiration.patchDownload
From 332ea5a58dcd6be7546e350eac1741b620d8e3cb Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Wed, 2 Mar 2022 06:07:40 -0800
Subject: [PATCH 3/3] Per-password expiration

To build on the multi-password support this
adds per-password expiration, either passed in via ALTER ROLE/CREATE
ROLE with the grammar EXPIRES IN, or via a system-wide setting
called password_valid_duration

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 121 ++++++++++++++++++++++++-
 src/backend/commands/variable.c        | 105 +++++++++++++++++++++
 src/backend/libpq/auth.c               |   9 +-
 src/backend/libpq/crypt.c              |  38 +++++++-
 src/backend/parser/gram.y              |  10 +-
 src/backend/utils/misc/guc.c           |  13 +++
 src/include/catalog/pg_auth_password.h |   1 +
 src/include/commands/user.h            |   3 +
 src/include/commands/variable.h        |   4 +
 src/include/parser/kwlist.h            |   1 +
 10 files changed, 292 insertions(+), 13 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 384816444da..2e3f4c04418 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -48,8 +48,9 @@
 Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 
 
-/* GUC parameter */
+/* GUC parameters */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+Interval *default_password_duration = NULL;
 
 /* default password name */
 const char* default_passname = "__def__";
@@ -167,6 +168,84 @@ validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
 	return true;
 }
 
+static
+Datum
+expires_in_datum(DefElem *passExpiresIn)
+{
+	Interval *interval;
+	Node	   *arg;
+	A_Const    *con;
+	TimestampTz now = GetCurrentTimestamp();
+	Datum		passExpiresIn_datum;
+	char *dateout;
+
+	if (default_password_duration != NULL) 
+	{
+		/* The default duration GUC is set, use it if nothing came from SQL
+		 * or if something came from SQL, reject it if not from a superuser
+		 */
+
+		if (passExpiresIn != NULL)
+			if (!superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("must be superuser to override password_validity_duration GUC")));
+			else
+				goto bypass;
+		else
+		{
+			passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										 PointerGetDatum(default_password_duration));
+
+			dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+			ereport(NOTICE,
+				(errmsg("Password will expire at: \"%s\" (from GUC)", dateout)));
+
+			return passExpiresIn_datum;
+		}
+	}
+
+	if (passExpiresIn == NULL) 	/* No duration requested via SQL and no system default, no expiration */
+		return PointerGetDatum(NULL);
+
+bypass:
+ 	arg = (Node *)passExpiresIn->arg;
+	if (IsA(arg, TypeCast))
+	{
+		TypeCast   *tc = (TypeCast *) passExpiresIn->arg;
+		arg = tc->arg;
+	}
+
+	if (!IsA(arg, A_Const))
+	{
+		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
+		return PointerGetDatum(NULL);
+	}
+	con = (A_Const *) arg;
+
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+								CStringGetDatum(strVal(&con->val)),
+								ObjectIdGetDatum(InvalidOid),
+								Int32GetDatum(-1)));
+
+	passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										PointerGetDatum(interval));
+
+	dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+	ereport(NOTICE,
+		(errmsg("Password will expire at: \"%s\" (from SQL)", dateout)));
+
+	return passExpiresIn_datum;
+}
+
 /*
  * CREATE ROLE
  */
@@ -211,7 +290,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
-
+	DefElem    *dpassExpiresIn = NULL;
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -320,6 +399,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 
 		else
 			elog(ERROR, "option \"%s\" not recognized",
@@ -508,6 +593,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
 		HeapTuple	new_tuple;
+		Datum		passExpiresIn_datum;
 
 		/*
 		 * Don't allow an empty password. Libpq treats an empty password the
@@ -529,19 +615,27 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+			if (passExpiresIn_datum != PointerGetDatum(NULL))
+				new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+			else
+				new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+	
 			/* Encrypt the password to the requested format. */
 			validate_and_get_salt(stmt->role, &salt, &logdetail);
 			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
-			MemSet(new_password_record, 0, sizeof(new_password_record));
-			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 			if (passname != NULL)
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(passname));
 			else
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -648,6 +742,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
 	bool		validUntil_null;
+	Datum		passExpiresIn_datum; /* Time period until password expires */
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
@@ -660,6 +755,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
+	DefElem	   *dpassExpiresIn = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -743,6 +839,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -763,6 +865,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (dpassName)
 		passname = strVal(dpassName->arg);
 
+
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
 	 */
@@ -849,6 +952,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
 
+	passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+	if (passExpiresIn_datum != PointerGetDatum(NULL)) {
+		new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+		new_password_record_repl[Anum_pg_auth_password_expiration - 1] = true;
+	}
+	else
+		new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
 
 	/*
 	 * issuper/createrole/etc
@@ -958,7 +1068,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
-	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true 
+		|| new_password_record_repl[Anum_pg_auth_password_expiration - 1] == true)
 	{
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4a..ca85a10b80b 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "commands/user.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -933,3 +934,107 @@ show_role(void)
 	/* Otherwise we can just use the GUC string */
 	return role_string ? role_string : "none";
 }
+
+
+/*
+ * check_password_duration: GUC check_hook for password_duration
+ */
+bool
+check_password_duration(char **newval, void **extra, GucSource source)
+{
+	Interval	*new_interval;
+	char	   *endptr;
+
+	const char *valueptr = *newval;
+	char	   *val;
+	Interval   *interval;
+
+	if (newval == NULL || *newval == NULL) {
+		extra = NULL;
+		return true;
+	}
+
+	elog(NOTICE,"Setting password duration to \"%s\"",
+					*newval);
+
+	while (isspace((unsigned char) *valueptr))
+		valueptr++;
+	if (*valueptr != '\'') {
+		val = pstrdup(valueptr);
+	}
+	else
+	{
+		valueptr++;
+		val = pstrdup(valueptr);
+		/* Check and remove trailing quote */
+		endptr = strchr(val, '\'');
+		if (!endptr || endptr[1] != '\0')
+		{
+			pfree(val);
+			return false;
+		}
+		*endptr = '\0';
+	}
+
+	/*
+		* Try to parse it.  XXX an invalid interval format will result in
+		* ereport(ERROR), which is not desirable for GUC.  We did what we
+		* could to guard against this in flatten_set_variable_args, but a
+		* string coming in from postgresql.conf might contain anything.
+		*/
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+														CStringGetDatum(val),
+														ObjectIdGetDatum(InvalidOid),
+														Int32GetDatum(-1)));
+
+	pfree(val);
+
+	if (!interval) {
+		return false;
+	}
+
+	new_interval = malloc(sizeof(Interval));
+	memcpy(new_interval, interval, sizeof(Interval));
+	pfree(interval);
+
+	/*
+	 * Pass back data for assign_password_validity to use
+	 */
+	*extra = malloc(sizeof(Interval *));
+	if (!*extra)
+		return false;
+	*((Interval **) *extra) = new_interval;
+
+	return true;
+}
+
+/*
+ * assign_password_validity: GUC assign_hook for timezone
+ */
+void
+assign_password_duration(const char *newval, void *extra)
+{
+	if (extra == NULL)
+		default_password_duration = NULL;
+	else
+		default_password_duration = *((Interval **) extra);
+}
+
+/*
+ * show_password_validity: GUC show_hook for timezone
+ */
+const char *
+show_password_duration(void)
+{
+	const char *intervalout;
+	if (default_password_duration == NULL) {
+		return "";
+	}
+	intervalout = DatumGetCString(DirectFunctionCall1(interval_out,
+										PointerGetDatum(default_password_duration)));
+
+	if (intervalout != NULL)
+		return intervalout;
+
+	return "";
+}
\ No newline at end of file
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5569599ab31..641cc92549b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -855,9 +855,14 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
 											logdetail);
 
-		for (i = 0; i < num; i++) 
-			pfree(passwords[i]);
+		for (i = 0; i < num; i++) {
 
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(LOG,
+					(errmsg("Password %d was null", i)));
+		}
 		pfree(passwords);
 	}
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 46b458f7d8a..13e0bf16506 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -40,10 +40,12 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
+	TimestampTz current, vuntil = 0;
+
 	bool		isnull;
 	char	   **passwords;
 	CatCList   *passlist;
-	int		    i;
+	int		    i, j = 0, num_valid_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -51,6 +53,7 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	{
 		*logdetail = psprintf(_("Role \"%s\" does not exist."),
 							  role);
+		*num = 0;
 		return NULL;			/* no such user */
 	}
 
@@ -58,7 +61,6 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	ReleaseSysCache(roleTup);
 	/* Find any existing password that is not the one being updated to get the salt */
 	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
-	*num = passlist->n_members;
 
 	if (passlist->n_members == 0)
 	{
@@ -68,15 +70,41 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 		return NULL;			/* user has no password */
 	}
 
-	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+	current = GetCurrentTimestamp();
+
+	for (i = 0; i < passlist->n_members; i++)
+	{
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+			num_valid_passwords++;
+	}
 
+	passwords = palloc0(sizeof(char *) * num_valid_passwords); 
+	*num = num_valid_passwords;
+	
 	for (i = 0; i < passlist->n_members; i++)
 	{
 		HeapTuple	tup = &passlist->members[i]->tuple;
 
 		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
-							Anum_pg_auth_password_password, &isnull);
-		passwords[i] = TextDatumGetCString(datum);
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+		{
+			datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+								Anum_pg_auth_password_password, &isnull);
+			passwords[j++] = pstrdup(TextDatumGetCString(datum));
+		}
 	}
 
 	ReleaseCatCacheList(passlist);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 2a288ff13f9..8e0887618d0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -682,7 +682,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DOUBLE_P DROP
 
 	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPIRES EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -1137,6 +1137,12 @@ AlterOptRoleElem:
 					$$ = makeDefElem("passname",
 									 (Node *)makeString($2), @1);
 				}
+			| EXPIRES IN_P Sconst opt_interval
+				{
+					TypeName *t = SystemTypeName("interval");
+					t->typmods = $4;
+					$$ = makeDefElem("expiresin", (Node *)makeStringConstCast($3, @3, t), @1);
+				}
 			| VALID FOR Sconst
 				{
 					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
@@ -15766,6 +15772,7 @@ unreserved_keyword:
 			| EXCLUDING
 			| EXCLUSIVE
 			| EXECUTE
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -16311,6 +16318,7 @@ bare_label_keyword:
 			| EXCLUSIVE
 			| EXECUTE
 			| EXISTS
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1e3650184b1..a1295a3070f 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -682,6 +682,7 @@ static char *recovery_target_string;
 static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
+static char *password_duration_string;
 
 
 /* should be static, but commands/variable.c needs to get at this */
@@ -4662,6 +4663,17 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"password_valid_duration", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Specifies the default validity duration of new passwords."),
+			NULL,
+			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
+		},
+		&password_duration_string,
+		NULL,
+		check_password_duration, assign_password_duration, show_password_duration
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
@@ -5039,6 +5051,7 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index 4181caad30b..1cb0a211ee7 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -31,6 +31,7 @@ CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_RO
 
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
     text            password BKI_FORCE_NOT_NULL;        /* password */
+    timestamptz     expiration BKI_FORCE_NULL;	        /* password expiration time, if any */
 #endif
 } FormData_pg_auth_password;
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 7223b1fdae4..1203358933a 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -19,6 +19,9 @@
 /* GUC. Is actually of type PasswordType. */
 extern int	Password_encryption;
 
+/* GUC. system-wide password validity duration */
+extern Interval *default_password_duration;
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/commands/variable.h b/src/include/commands/variable.h
index 0e5ddcbcf37..3b941c554cb 100644
--- a/src/include/commands/variable.h
+++ b/src/include/commands/variable.h
@@ -34,5 +34,9 @@ extern void assign_session_authorization(const char *newval, void *extra);
 extern bool check_role(char **newval, void **extra, GucSource source);
 extern void assign_role(const char *newval, void *extra);
 extern const char *show_role(void);
+extern bool check_password_duration(char **newval, void **extra, GucSource source);
+extern void assign_password_duration(const char *newval, void *extra);
+extern const char *show_password_duration(void);
+
 
 #endif							/* VARIABLE_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index e7239b34885..0e1c8098363 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -159,6 +159,7 @@ PG_KEYWORD("excluding", EXCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exclusive", EXCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("execute", EXECUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exists", EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("expires", EXPIRES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("explain", EXPLAIN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("expression", EXPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("extension", EXTENSION, UNRESERVED_KEYWORD, BARE_LABEL)
-- 
2.31.1

v1-0002-multiple-passwords-work-with-scram-and-md5.patchapplication/octet-stream; name=v1-0002-multiple-passwords-work-with-scram-and-md5.patchDownload
From 6f2451e1f620e7e211804792a256ea9735bd0abf Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Tue, 1 Feb 2022 14:32:55 -0800
Subject: [PATCH 2/3] multiple passwords work with scram and md5

also renaming roles, dropping roles, switching between
password_encryption

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 199 +++++++++++++++++++++----
 src/backend/libpq/auth-sasl.c          |   4 +-
 src/backend/libpq/auth-scram.c         | 184 +++++++++++++----------
 src/backend/libpq/auth.c               |  94 +++++-------
 src/backend/libpq/crypt.c              |  50 ++++---
 src/backend/parser/gram.y              |  13 +-
 src/backend/utils/cache/catcache.c     |   2 +-
 src/backend/utils/cache/syscache.c     |   6 +-
 src/include/catalog/pg_auth_password.h |   9 +-
 src/include/commands/user.h            |   2 +-
 src/include/libpq/crypt.h              |   2 +-
 src/include/libpq/sasl.h               |   4 +-
 src/include/libpq/scram.h              |   2 +-
 src/include/parser/kwlist.h            |   1 +
 src/include/utils/syscache.h           |   2 +-
 src/test/regress/expected/password.out |   2 +-
 16 files changed, 373 insertions(+), 203 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 9ff99342eef..384816444da 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -18,6 +18,7 @@
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
@@ -31,11 +32,14 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/fmgroids.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -47,6 +51,9 @@ Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 /* GUC parameter */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
 
+/* default password name */
+const char* default_passname = "__def__";
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 check_password_hook_type check_password_hook = NULL;
 
@@ -69,7 +76,7 @@ have_createrole_privilege(void)
  * Is the role able to log in
 */
 bool
-is_role_valid(const char *rolename, char **logdetail)
+is_role_valid(const char *rolename, const char **logdetail)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
@@ -102,6 +109,64 @@ is_role_valid(const char *rolename, char **logdetail)
 }
 
 
+static
+bool
+validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
+{
+	char	  **current_secrets;
+	char	   *salt1, *salt2 = NULL;
+	int			i, num;
+	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);
+	if (num == 0)
+	{
+		*salt = NULL; /* No existing passwords, allow one to be generated */
+		return true;
+	}
+	for (i = 0; i < num; 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;
+				uint8		stored_key[SCRAM_KEY_LEN];
+				uint8		server_key[SCRAM_KEY_LEN];
+
+				parse_scram_secret(current_secrets[i], &iterations, &salt1,
+									stored_key, server_key);
+
+				if (salt2 != NULL) {
+					if (strcmp(salt1, salt2)) {
+						*logdetail = psprintf(_("inconsistent salts, clearing password"));
+						*salt = NULL;
+						return false;
+					}
+				}
+				else
+					salt2 = salt1;
+		}
+
+	}
+	for (i = 0; i < num; i++)
+		pfree(current_secrets[i]);
+	pfree(current_secrets);
+	if (salt2)
+		*salt = pstrdup(salt2);
+	return true;
+}
+
 /*
  * CREATE ROLE
  */
@@ -117,6 +182,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	ListCell   *item;
 	ListCell   *option;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* name of the password for managing multiple passwords */
 	bool		issuper = false;	/* Make the user a superuser? */
 	bool		inherit = true; /* Auto inherit privileges? */
 	bool		createrole = false; /* Can this user create roles? */
@@ -144,6 +210,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dadminmembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -246,6 +314,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -283,6 +358,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		validUntil = strVal(dvalidUntil->arg);
 	if (dbypassRLS)
 		bypassrls = boolVal(dbypassRLS->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/* Check some permissions first */
 	if (issuper)
@@ -424,8 +501,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 	if (password)
 	{
-		char	   *shadow_pass;
-		char	   *logdetail;
+		char	   *shadow_pass, *salt;
+		const char	   *logdetail;
 		Datum		new_password_record[Natts_pg_auth_password];
 		bool		new_password_record_nulls[Natts_pg_auth_password];
 		Relation	pg_auth_password_rel;
@@ -453,12 +530,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		else
 		{
 			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+			validate_and_get_salt(stmt->role, &salt, &logdetail);
+			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
 			MemSet(new_password_record, 0, sizeof(new_password_record));
 			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
-
+			if (passname != NULL)
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(passname));
+			else
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -560,6 +643,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* password name for managing multiple passwords */
 	int			connlimit = -1; /* maximum connections allowed */
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
@@ -575,6 +659,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -652,6 +737,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -669,6 +760,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -805,8 +898,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	/* password */
 	if (password)
 	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
+		char	*salt = NULL;
+		const char 	*logdetail = NULL;
+		char	*shadow_pass;
 
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (password[0] == '\0' ||
@@ -818,13 +912,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else
 		{
-			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
-			new_password_record[Anum_pg_auth_password_password - 1] =
-				CStringGetTextDatum(shadow_pass);
+			if (!validate_and_get_salt(rolename, &salt, &logdetail))
+				new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+			else
+			{
+				/* Encrypt the password to the requested format. */
+				shadow_pass = encrypt_password(Password_encryption, salt,
+											password);
+				new_password_record[Anum_pg_auth_password_password - 1] =
+					CStringGetTextDatum(shadow_pass);
+
+				new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+				if (passname)
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(passname));
+				else
+					new_password_record[Anum_pg_auth_password_name - 1] =
+					DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+			}
 		}
-		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
@@ -859,7 +966,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
-		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+		if (dpassName)
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+		else
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname));
 
 		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
 		{
@@ -1157,14 +1267,23 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
 		/*
-		 * Drop password
+		 * Drop password(s)
 		 */
-		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
-		if (HeapTupleIsValid(tuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
-			ReleaseSysCache(tuple);
+		ScanKeyInit(&scankey,
+					Anum_pg_auth_password_roleid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(roleid));
+
+		sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								   true, NULL, 1, &scankey);
+
+		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+		{
+			CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
 		}
 
+		systable_endscan(sscan);
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1201,6 +1320,9 @@ RenameRole(const char *oldname, const char *newname)
 				newtuple,
 				passtuple;
 	TupleDesc	dsc;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+
 	Relation	rel;
 	Datum		datum;
 	bool		isnull = true;
@@ -1211,6 +1333,7 @@ RenameRole(const char *oldname, const char *newname)
 	Oid			roleid;
 	ObjectAddress address;
 	Form_pg_authid authform;
+	Relation	pg_auth_password_rel;
 
 	rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	dsc = RelationGetDescr(rel);
@@ -1301,28 +1424,38 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_password_roleid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(roleid));
+
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
-	if (HeapTupleIsValid(passtuple))
-		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
-								Anum_pg_auth_password_password, &isnull);
+	sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								true, NULL, 1, &scankey);
 
-	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+	while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
 	{
-		Relation	pg_auth_password_rel;
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+							Anum_pg_auth_password_password, &isnull);
 
-		/* MD5 uses the username as salt, so just clear it on a rename */
-		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+		{
 
-		if (HeapTupleIsValid(passtuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
-			ReleaseSysCache(passtuple);
+			/* MD5 uses the username as salt, so just clear it on a rename */
+
+			if (HeapTupleIsValid(passtuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+				ereport(NOTICE,
+					(errmsg("MD5 password cleared because of role rename")));
+
+			}
 		}
-		table_close(pg_auth_password_rel, NoLock);
-		ereport(NOTICE,
-				(errmsg("MD5 password cleared because of role rename")));
 	}
 
+	systable_endscan(sscan);
+	table_close(pg_auth_password_rel, NoLock);
+
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
 
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 805b3695b78..652c18fa94a 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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,
 			  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);
 
 			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 795f1cba555..95db4aaf958 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,13 @@ typedef enum
 	SCRAM_AUTH_FINISHED
 } scram_state_enum;
 
+typedef struct
+{
+	uint8		StoredKey[SCRAM_KEY_LEN];
+	uint8		ServerKey[SCRAM_KEY_LEN];
+} scram_secret;
+
+
 typedef struct
 {
 	scram_state_enum state;
@@ -141,10 +148,16 @@ typedef struct
 	Port	   *port;
 	bool		channel_binding_in_use;
 
+	/* 
+	 * 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_KEY_LEN];
-	uint8		ServerKey[SCRAM_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;
@@ -220,17 +233,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_auth_password.password.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
  * 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;
@@ -260,46 +276,48 @@ 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->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->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 */
+							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;
 	}
 
 	/*
@@ -310,8 +328,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 */
 	if (!got_secret)
 	{
+		state->secrets = palloc0(sizeof(scram_secret));
+
 		mock_scram_secret(state->port->user_name, &state->iterations,
-						  &state->salt, state->StoredKey, state->ServerKey);
+						  &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey);
 		state->doomed = true;
 	}
 
@@ -459,7 +479,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -476,12 +496,14 @@ 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)
+		pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN);
+	
 	result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
 								SCRAM_DEFAULT_ITERATIONS, password,
 								&errstr);
@@ -1114,47 +1136,57 @@ verify_client_proof(scram_state *state)
 	uint8		ClientSignature[SCRAM_KEY_LEN];
 	uint8		ClientKey[SCRAM_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_KEY_LEN];
-	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
-	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, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 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(PG_SHA256);
+		elog(LOG, "Trying to verify password %d", j);
+
+		if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 0)
+		{
+			elog(LOG, "could not calculate client signature");
+			pg_hmac_free(ctx);
+			continue;
+		}
 
-	pg_hmac_free(ctx);
+		elog(LOG, "success on %d", j);
 
-	/* Extract the ClientKey that the client calculated from the proof */
-	for (i = 0; i < SCRAM_KEY_LEN; 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, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
-		elog(ERROR, "could not hash stored key: %s", errstr);
+		for (i = 0; i < SCRAM_KEY_LEN; i++)
+			ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
-	if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
-		return false;
+		/* Hash it one more time, and compare with StoredKey */
+		if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
+			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	return true;
+		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) {
+			elog(LOG, "Moving forward with Password %d", j);
+			state->chosen_secret = j;
+			return true; 
+		}
+	}
+
+	return false;
 }
 
 /*
@@ -1380,7 +1412,7 @@ build_server_final_message(scram_state *state)
 	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
 
 	/* calculate ServerSignature */
-	if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 ||
+	if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 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 5cfde1eaa13..5569599ab31 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -58,8 +58,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, const char **logdetail);
 
 
 /*----------------------------------------------------------------
@@ -790,8 +789,9 @@ static int
 CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
-	int			result;
-	char	   *shadow_pass;
+	int			result = STATUS_ERROR;
+	int			i, num;
+	char	   **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -799,17 +799,21 @@ 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)
-	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
-	}
-	else
-		result = STATUS_ERROR;
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
+	if (passwords != NULL) {
+		for (i = 0; i < num; 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; passwords[i] != NULL; i++) 
+			pfree(passwords[i]);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		pfree(passwords);
+	}
+	
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -824,29 +828,15 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 static int
 CheckPWChallengeAuth(Port *port, const char **logdetail)
 {
-	int			auth_result;
-	char	   *shadow_pass;
-	PasswordType pwtype;
+	int			auth_result = STATUS_ERROR;
+	int			i, num;
+	char	  **passwords;
 
 	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);
-
-	/*
-	 * 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
-	 * 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)
-		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
 
 	/*
 	 * If 'md5' authentication is allowed, decide whether to perform 'md5' or
@@ -858,23 +848,17 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 	 * 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);
-	else
-		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
-									logdetail);
+	if (passwords != NULL) {
+		if (port->hba->auth_method == uaMD5)
+			auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
+											logdetail);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		for (i = 0; i < num; i++) 
+			pfree(passwords[i]);
 
-	/*
-	 * If get_role_password() returned error, return error, even if the
-	 * authentication succeeded.
-	 */
-	if (!shadow_pass)
-	{
-		Assert(auth_result != STATUS_OK);
-		return STATUS_ERROR;
+		pfree(passwords);
 	}
 
 	if (auth_result == STATUS_OK)
@@ -884,11 +868,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, const char **logdetail)
 {
 	char		md5Salt[4];		/* Password salt */
 	char	   *passwd;
 	int			result;
+	int			i;
 
 	if (Db_user_namespace)
 		ereport(FATAL,
@@ -909,12 +894,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; 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 745e61034c2..46b458f7d8a 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -23,6 +23,7 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
@@ -34,13 +35,15 @@
  * 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)
 {
-	HeapTuple	roleTup, passTup;
+	HeapTuple	roleTup;
 	Datum		datum;
 	bool		isnull;
-	char	   *shadow_pass;
+	char	   **passwords;
+	CatCList   *passlist;
+	int		    i;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,30 +55,33 @@ get_role_password(const char *role, const char **logdetail)
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
-	
-	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+	ReleaseSysCache(roleTup);
+	/* Find any existing password that is not the one being updated to get the salt */
+	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+	*num = passlist->n_members;
 
-	if (!HeapTupleIsValid(passTup))
+	if (passlist->n_members == 0)
 	{
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
+		ReleaseCatCacheList(passlist);
 		return NULL;			/* user has no password */
 	}
-	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
-							Anum_pg_auth_password_password, &isnull);
 
-	ReleaseSysCache(roleTup);
-	if (isnull) /* this should not happen any more but just in case */
+	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+
+	for (i = 0; i < passlist->n_members; i++)
 	{
-		ReleaseSysCache(passTup);
-		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
-							  role);
-		return NULL;			/* user has no password */
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_password, &isnull);
+		passwords[i] = TextDatumGetCString(datum);
 	}
-	shadow_pass = TextDatumGetCString(datum);
-	ReleaseSysCache(passTup);
 
-	return shadow_pass;
+	ReleaseCatCacheList(passlist);
+
+	return passwords;
 }
 
 /*
@@ -107,7 +113,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);
@@ -128,13 +134,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);
+				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'");
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a03b33b53bd..2a288ff13f9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -716,7 +716,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
+	PARALLEL PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -1132,6 +1132,15 @@ AlterOptRoleElem:
 							 errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
 							 parser_errposition(@1)));
 				}
+			| PASSNAME Sconst
+				{
+					$$ = makeDefElem("passname",
+									 (Node *)makeString($2), @1);
+				}
+			| VALID FOR Sconst
+				{
+					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+				}
 			| INHERIT
 				{
 					$$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
@@ -15855,6 +15864,7 @@ unreserved_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLANS
 			| POLICY
@@ -16433,6 +16443,7 @@ bare_label_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLACING
 			| PLANS
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 356e9cde6fb..c421ac0b19f 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHPASSWORD:
+		case AUTHPASSWORDNAME:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 49d49f13252..2c39cfb083f 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -255,12 +255,12 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
-	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+	{AuthPasswordRelationId,			/* AUTHPASSWORDNAME */
 		AuthPasswordRoleOidIndexId,
-		1,
+		2,
 		{
 			Anum_pg_auth_password_roleid,
-			0,
+			Anum_pg_auth_password_name,
 			0,
 			0
 		},
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index beaa2d40b90..4181caad30b 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -24,11 +24,13 @@
  *		typedef struct FormData_pg_auth_password
  * ----------------
  */
-CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
 {
 	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+    NameData    name;            /* name of password for multiple password support */
+
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
-    text            password;        /* password */
+    text            password BKI_FORCE_NOT_NULL;        /* password */
 #endif
 } FormData_pg_auth_password;
 
@@ -44,7 +46,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176);
 
 typedef FormData_pg_auth_password *Form_pg_auth_password;
 
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
-
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops));
 
 #endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index ce39db5a491..7223b1fdae4 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
-extern bool is_role_valid(const char *rolename, char **logdetail);
+extern bool is_role_valid(const char *rolename, const char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index b8ff8ccb417..84c9c4b4f48 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 71cc0dc2514..9892f9aad38 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, const char **logdetail);
 
 #endif							/* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index e60992a0d2d..8cc6f492eeb 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,7 @@
 extern 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, char **salt,
 							   uint8 *stored_key, uint8 *server_key);
 extern bool scram_verify_plain_password(const char *username,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bcef7eed2f3..e7239b34885 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -311,6 +311,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index ab846e8b7fa..869cc4e4e20 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,7 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
-	AUTHPASSWORD,
+	AUTHPASSWORDNAME,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 4ffc41a5455..c6a84f86a84 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 -- should not contain the original salt.
 SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
-- 
2.31.1

v1-0001-Move-rolpassword-out-of-pg_authid-into-a-new-table.patchapplication/octet-stream; name=v1-0001-Move-rolpassword-out-of-pg_authid-into-a-new-table.patchDownload
From 9a4d6a7bb5c8e18e60ea1f7e747f5e024d2e6573 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Thu, 9 Dec 2021 16:18:25 -0800
Subject: [PATCH 1/3] Move rolpassword out of pg_authid into a new table

In preparation for more flexibility in password
management the rolpassword column needs to be
moved into a new table.

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/catalog/Makefile                 |   2 +-
 src/backend/catalog/catalog.c                |   7 +-
 src/backend/catalog/system_views.sql         |   5 +-
 src/backend/commands/user.c                  | 226 ++++++++++++----
 src/backend/libpq/auth-sasl.c                |   2 +-
 src/backend/libpq/auth-scram.c               |   4 +-
 src/backend/libpq/auth.c                     |  16 ++
 src/backend/libpq/crypt.c                    |  40 ++-
 src/backend/utils/cache/catcache.c           |   1 +
 src/backend/utils/cache/relcache.c           |  13 +-
 src/backend/utils/cache/syscache.c           |  12 +
 src/bin/initdb/initdb.c                      |   2 +-
 src/bin/pg_dump/pg_dumpall.c                 |  13 +-
 src/common/scram-common.c                    |   2 +-
 src/include/catalog/pg_auth_password.h       |  50 ++++
 src/include/catalog/pg_authid.dat            |  26 +-
 src/include/catalog/pg_authid.h              |   9 +-
 src/include/commands/user.h                  |   1 +
 src/include/libpq/crypt.h                    |   2 +-
 src/include/utils/syscache.h                 |   1 +
 src/test/regress/expected/create_index.out   |  14 +-
 src/test/regress/expected/oidjoins.out       |   1 +
 src/test/regress/expected/password.out       |  44 ++--
 src/test/regress/expected/roleattributes.out | 256 +++++++++----------
 src/test/regress/expected/rules.out          |   5 +-
 src/test/regress/expected/tablespace.out     |  12 +-
 src/test/regress/sql/create_index.sql        |  10 +-
 src/test/regress/sql/password.sql            |  32 ++-
 src/test/regress/sql/roleattributes.sql      |  64 ++---
 src/test/regress/sql/tablespace.sql          |   8 +-
 30 files changed, 562 insertions(+), 318 deletions(-)
 create mode 100644 src/include/catalog/pg_auth_password.h

diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index eefebb7bb83..ea6d7e47340 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -61,7 +61,7 @@ CATALOG_HEADERS := \
 	pg_rewrite.h pg_trigger.h pg_event_trigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h \
-	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
+	pg_authid.h pg_auth_members.h pg_auth_password.h pg_shdepend.h pg_shdescription.h \
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index dfd5fb669ee..410b3328011 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -28,6 +28,7 @@
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
@@ -245,6 +246,7 @@ IsSharedRelation(Oid relationId)
 {
 	/* These are the shared catalogs (look for BKI_SHARED_RELATION) */
 	if (relationId == AuthIdRelationId ||
+		relationId == AuthPasswordRelationId ||
 		relationId == AuthMemRelationId ||
 		relationId == DatabaseRelationId ||
 		relationId == SharedDescriptionRelationId ||
@@ -258,6 +260,7 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdRolnameIndexId ||
 		relationId == AuthIdOidIndexId ||
+		relationId == AuthPasswordRoleOidIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == DatabaseNameIndexId ||
@@ -275,8 +278,8 @@ IsSharedRelation(Oid relationId)
 		relationId == SubscriptionNameIndexId)
 		return true;
 	/* These are their toast tables and toast indexes */
-	if (relationId == PgAuthidToastTable ||
-		relationId == PgAuthidToastIndex ||
+	if (relationId == PgAuthPasswordToastTable ||
+		relationId == PgAuthPasswordToastIndex ||
 		relationId == PgDatabaseToastTable ||
 		relationId == PgDatabaseToastIndex ||
 		relationId == PgDbRoleSettingToastTable ||
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 40b7bca5a96..4dca955372a 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -40,11 +40,14 @@ CREATE VIEW pg_shadow AS
         rolsuper AS usesuper,
         rolreplication AS userepl,
         rolbypassrls AS usebypassrls,
-        rolpassword AS passwd,
+        p.password AS passwd,
         rolvaliduntil AS valuntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
+    LEFT JOIN pg_auth_password p
+    ON p.roleid = pg_authid.oid
+
     WHERE rolcanlogin;
 
 REVOKE ALL ON pg_shadow FROM public;
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246bb..9ff99342eef 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -23,6 +23,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "commands/comment.h"
@@ -64,6 +65,42 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+/*
+ * Is the role able to log in
+*/
+bool
+is_role_valid(const char *rolename, char **logdetail)
+{
+	HeapTuple	roleTup;
+	Datum		datum;
+	bool		isnull;
+	TimestampTz vuntil = 0;
+
+	/* Get role info from pg_authid */
+	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(rolename));
+	if (!HeapTupleIsValid(roleTup))
+	{
+		*logdetail = psprintf(_("Role \"%s\" does not exist."),
+							  rolename);
+		return false;			/* no such user */
+	}
+
+	datum = SysCacheGetAttr(AUTHNAME, roleTup,
+							Anum_pg_authid_rolvaliduntil, &isnull);
+	ReleaseSysCache(roleTup);
+
+	if (!isnull)
+		vuntil = DatumGetTimestampTz(datum);
+
+	if (!isnull && vuntil < GetCurrentTimestamp())
+	{
+		*logdetail = psprintf(_("User \"%s\" has an expired password."), rolename);
+		return false;
+	}
+
+	return true;
+}
+
 
 /*
  * CREATE ROLE
@@ -351,43 +388,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
 	new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
 	new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
-
-	if (password)
-	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
-
-		/*
-		 * Don't allow an empty password. Libpq treats an empty password the
-		 * same as no password at all, and won't even try to authenticate. But
-		 * other clients might, so allowing it would be confusing. By clearing
-		 * the password when an empty string is specified, the account is
-		 * consistently locked for all clients.
-		 *
-		 * Note that this only covers passwords stored in the database itself.
-		 * There are also checks in the authentication code, to forbid an
-		 * empty password from being used with authentication methods that
-		 * fetch the password from an external system, like LDAP or PAM.
-		 */
-		if (password[0] == '\0' ||
-			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
-		{
-			ereport(NOTICE,
-					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-		}
-		else
-		{
-			/* 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);
-		}
-	}
-	else
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
@@ -422,6 +422,60 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	CatalogTupleInsert(pg_authid_rel, tuple);
 
+	if (password)
+	{
+		char	   *shadow_pass;
+		char	   *logdetail;
+		Datum		new_password_record[Natts_pg_auth_password];
+		bool		new_password_record_nulls[Natts_pg_auth_password];
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple	new_tuple;
+
+		/*
+		 * Don't allow an empty password. Libpq treats an empty password the
+		 * same as no password at all, and won't even try to authenticate. But
+		 * other clients might, so allowing it would be confusing. By clearing
+		 * the password when an empty string is specified, the account is
+		 * consistently locked for all clients.
+		 *
+		 * Note that this only covers passwords stored in the database itself.
+		 * There are also checks in the authentication code, to forbid an
+		 * empty password from being used with authentication methods that
+		 * fetch the password from an external system, like LDAP or PAM.
+		 */
+		if (password[0] == '\0' ||
+			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+		}
+		else
+		{
+			/* Encrypt the password to the requested format. */
+			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+										   password);
+
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			/* open password table and insert it. */
+			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+
+			new_password_record[Anum_pg_auth_password_password - 1] =
+				CStringGetTextDatum(shadow_pass);
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+			table_close(pg_auth_password_rel, NoLock);
+
+		}
+	}
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -495,6 +549,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	Datum		new_record[Natts_pg_authid];
 	bool		new_record_nulls[Natts_pg_authid];
 	bool		new_record_repl[Natts_pg_authid];
+	Datum		new_password_record[Natts_pg_auth_password];
+	bool		new_password_record_nulls[Natts_pg_auth_password];
+	bool		new_password_record_repl[Natts_pg_auth_password];
 	Relation	pg_authid_rel;
 	TupleDesc	pg_authid_dsc;
 	HeapTuple	tuple,
@@ -624,6 +681,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	rolename = pstrdup(NameStr(authform->rolname));
 	roleid = authform->oid;
 
+
 	/*
 	 * To mess with a superuser or replication role in any way you gotta be
 	 * superuser.  We also insist on superuser to change the BYPASSRLS
@@ -694,6 +752,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_record, 0, sizeof(new_record));
 	MemSet(new_record_nulls, false, sizeof(new_record_nulls));
 	MemSet(new_record_repl, false, sizeof(new_record_repl));
+	MemSet(new_password_record, 0, sizeof(new_password_record));
+	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
+
 
 	/*
 	 * issuper/createrole/etc
@@ -752,24 +814,24 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		{
 			ereport(NOTICE,
 					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+			new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 		}
 		else
 		{
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, rolename,
 										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
+			new_password_record[Anum_pg_auth_password_password - 1] =
 				CStringGetTextDatum(shadow_pass);
 		}
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
 	if (dpassword && dpassword->arg == NULL)
 	{
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+		new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* valid until */
@@ -786,12 +848,48 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
 								  new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
-
-	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
-
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	{
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple   password_tuple;
+
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+
+		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
+		{
+			if (HeapTupleIsValid(password_tuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &password_tuple->t_self);
+				ReleaseSysCache(password_tuple);
+			}
+		}
+		else if (HeapTupleIsValid(password_tuple)) /* update existing password */
+		{
+			new_tuple = heap_modify_tuple(password_tuple, pg_auth_password_dsc, new_password_record,
+										new_password_record_nulls, new_password_record_repl);
+			CatalogTupleUpdate(pg_auth_password_rel, &password_tuple->t_self, new_tuple);
+			ReleaseSysCache(password_tuple);
+			heap_freetuple(new_tuple);
+		}
+		else /* insert new password */
+		{
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+		}
+
+		table_close(pg_auth_password_rel, NoLock);
+	}
+	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -901,7 +999,6 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 	return roleid;
 }
 
-
 /*
  * DROP ROLE
  */
@@ -909,7 +1006,8 @@ void
 DropRole(DropRoleStmt *stmt)
 {
 	Relation	pg_authid_rel,
-				pg_auth_members_rel;
+				pg_auth_members_rel,
+				pg_auth_password_rel;
 	ListCell   *item;
 
 	if (!have_createrole_privilege())
@@ -923,6 +1021,8 @@ DropRole(DropRoleStmt *stmt)
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+
 
 	foreach(item, stmt->roles)
 	{
@@ -1056,6 +1156,15 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedComments(roleid, AuthIdRelationId);
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
+		/*
+		 * Drop password
+		 */
+		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
+		if (HeapTupleIsValid(tuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
+			ReleaseSysCache(tuple);
+		}
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1077,7 +1186,9 @@ DropRole(DropRoleStmt *stmt)
 	 * Now we can clean up; but keep locks until commit.
 	 */
 	table_close(pg_auth_members_rel, NoLock);
+	table_close(pg_auth_password_rel, NoLock);
 	table_close(pg_authid_rel, NoLock);
+
 }
 
 /*
@@ -1087,11 +1198,12 @@ ObjectAddress
 RenameRole(const char *oldname, const char *newname)
 {
 	HeapTuple	oldtuple,
-				newtuple;
+				newtuple,
+				passtuple;
 	TupleDesc	dsc;
 	Relation	rel;
 	Datum		datum;
-	bool		isnull;
+	bool		isnull = true;
 	Datum		repl_val[Natts_pg_authid];
 	bool		repl_null[Natts_pg_authid];
 	bool		repl_repl[Natts_pg_authid];
@@ -1189,14 +1301,24 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
+	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+
+	if (HeapTupleIsValid(passtuple))
+		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
+								Anum_pg_auth_password_password, &isnull);
 
 	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
 	{
+		Relation	pg_auth_password_rel;
+
 		/* MD5 uses the username as salt, so just clear it on a rename */
-		repl_repl[Anum_pg_authid_rolpassword - 1] = true;
-		repl_null[Anum_pg_authid_rolpassword - 1] = true;
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
+		if (HeapTupleIsValid(passtuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+			ReleaseSysCache(passtuple);
+		}
+		table_close(pg_auth_password_rel, NoLock);
 		ereport(NOTICE,
 				(errmsg("MD5 password cleared because of role rename")));
 	}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index a1d7dbb6d58..805b3695b78 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -33,7 +33,7 @@
  * implementation.
  *
  * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
+ * authenticated, from pg_auth_password.password.  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.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218ab..795f1cba555 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -220,7 +220,7 @@ 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.
+ * 'shadow_pass' is the role's stored secret, from pg_auth_password.password.
  * 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
@@ -454,7 +454,7 @@ 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_auth_password.password.
  *
  * The result is palloc'd, so caller is responsible for freeing it.
  */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f31353..5cfde1eaa13 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -592,10 +592,26 @@ ClientAuthentication(Port *port)
 
 		case uaMD5:
 		case uaSCRAM:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPWChallengeAuth(port, &logdetail);
 			break;
 
 		case uaPassword:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPasswordAuth(port, &logdetail);
 			break;
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 1ff8b0507d4..745e61034c2 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_auth_password.password.
  *
  * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -16,6 +16,7 @@
 #include <unistd.h>
 
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "common/md5.h"
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
@@ -36,8 +37,7 @@
 char *
 get_role_password(const char *role, const char **logdetail)
 {
-	TimestampTz vuntil = 0;
-	HeapTuple	roleTup;
+	HeapTuple	roleTup, passTup;
 	Datum		datum;
 	bool		isnull;
 	char	   *shadow_pass;
@@ -51,33 +51,29 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;			/* no such user */
 	}
 
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
+	
+	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+
+	if (!HeapTupleIsValid(passTup))
 	{
-		ReleaseSysCache(roleTup);
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
-
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
-	if (!isnull)
-		vuntil = DatumGetTimestampTz(datum);
+	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
+							Anum_pg_auth_password_password, &isnull);
 
 	ReleaseSysCache(roleTup);
-
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (isnull) /* this should not happen any more but just in case */
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
+		ReleaseSysCache(passTup);
+		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
-		return NULL;
+		return NULL;			/* user has no password */
 	}
+	shadow_pass = TextDatumGetCString(datum);
+	ReleaseSysCache(passTup);
 
 	return shadow_pass;
 }
@@ -156,7 +152,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_auth_password.password.
  * '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.
  *
@@ -211,7 +207,7 @@ 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_auth_password.password.
  * '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/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index ec073e1ed06..356e9cde6fb 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,6 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
+		case AUTHPASSWORD:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index fccffce5729..71773cb992c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_namespace.h"
@@ -112,6 +113,7 @@ static const FormData_pg_attribute Desc_pg_proc[Natts_pg_proc] = {Schema_pg_proc
 static const FormData_pg_attribute Desc_pg_type[Natts_pg_type] = {Schema_pg_type};
 static const FormData_pg_attribute Desc_pg_database[Natts_pg_database] = {Schema_pg_database};
 static const FormData_pg_attribute Desc_pg_authid[Natts_pg_authid] = {Schema_pg_authid};
+static const FormData_pg_attribute Desc_pg_auth_password[Natts_pg_auth_password] = {Schema_pg_auth_password};
 static const FormData_pg_attribute Desc_pg_auth_members[Natts_pg_auth_members] = {Schema_pg_auth_members};
 static const FormData_pg_attribute Desc_pg_index[Natts_pg_index] = {Schema_pg_index};
 static const FormData_pg_attribute Desc_pg_shseclabel[Natts_pg_shseclabel] = {Schema_pg_shseclabel};
@@ -3461,6 +3463,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case DatabaseRelationId:
 		case AuthIdRelationId:
+		case AuthPasswordRelationId:
 		case AuthMemRelationId:
 		case RelationRelationId:
 		case AttributeRelationId:
@@ -3881,7 +3884,7 @@ RelationCacheInitialize(void)
  *		RelationCacheInitializePhase2
  *
  *		This is called to prepare for access to shared catalogs during startup.
- *		We must at least set up nailed reldescs for pg_database, pg_authid,
+ *		We must at least set up nailed reldescs for pg_database, pg_authid, pg_auth_password,
  *		pg_auth_members, and pg_shseclabel. Ideally we'd like to have reldescs
  *		for their indexes, too.  We attempt to load this information from the
  *		shared relcache init file.  If that's missing or broken, just make
@@ -3926,8 +3929,10 @@ RelationCacheInitializePhase2(void)
 				  Natts_pg_shseclabel, Desc_pg_shseclabel);
 		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
 				  Natts_pg_subscription, Desc_pg_subscription);
+		formrdesc("pg_auth_password", AuthPasswordRelation_Rowtype_Id, true,
+				  Natts_pg_auth_password, Desc_pg_auth_password);
 
-#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_RELS	6	/* fix if you change list above */
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -4066,8 +4071,10 @@ RelationCacheInitializePhase3(void)
 							AuthMemRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
+		load_critical_index(AuthPasswordRoleOidIndexId,
+							AuthPasswordRelationId);
 
-#define NUM_CRITICAL_SHARED_INDEXES 6	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_INDEXES 7	/* fix if you change list above */
 
 		criticalSharedRelcachesBuilt = true;
 	}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4e7819f1e2..49d49f13252 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -254,6 +255,17 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+		AuthPasswordRoleOidIndexId,
+		1,
+		{
+			Anum_pg_auth_password_roleid,
+			0,
+			0,
+			0
+		},
+		8
+	},
 	{
 		CastRelationId,			/* CASTSOURCETARGET */
 		CastSourceTargetIndexId,
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 97f15971e2b..8958e07fd30 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1444,7 +1444,7 @@ setup_auth(FILE *cmdfd)
 		 * The authid table shouldn't be readable except through views, to
 		 * ensure passwords are not publicly visible.
 		 */
-		"REVOKE ALL ON pg_authid FROM public;\n\n",
+		"REVOKE ALL ON pg_auth_password FROM public;\n\n",
 		NULL
 	};
 
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9c9f7c6d63c..eb259d2b518 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -753,7 +753,18 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* note: rolconfig is dumped later */
-	if (server_version >= 90600)
+	if (server_version >= 150000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, p.password as rolpassword, "
+						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s LEFT JOIN pg_auth_password p ON %s.oid = p.roleid "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog, role_catalog, role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index 12686259299..d9d0cba7379 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -181,7 +181,7 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result,
 
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_auth_password.password.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
new file mode 100644
index 00000000000..beaa2d40b90
--- /dev/null
+++ b/src/include/catalog/pg_auth_password.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_auth_password.h
+ *	  definition of the "authorization identifier" system catalog (pg_auth_password)
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_auth_password.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_AUTH_PASSWORD_H
+#define PG_AUTH_PASSWORD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_auth_password_d.h"
+
+/* ----------------
+ *		pg_auth_password definition.  cpp turns this into
+ *		typedef struct FormData_pg_auth_password
+ * ----------------
+ */
+CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+{
+	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+#ifdef CATALOG_VARLEN                /* variable-length fields start here */
+    text            password;        /* password */
+#endif
+} FormData_pg_auth_password;
+
+/* ----------------
+ *		Form_pg_auth_password corresponds to a pointer to a tuple with
+ *		the format of pg_auth_password relation.
+ * ----------------
+ */
+
+DECLARE_TOAST(pg_auth_password, 4175, 4176);
+#define PgAuthPasswordToastTable 4175
+#define PgAuthPasswordToastIndex 4176
+
+typedef FormData_pg_auth_password *Form_pg_auth_password;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
+
+
+#endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6c28119fa1a..da36375a582 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,66 +23,66 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_null_' },
 { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINTER',
   rolname => 'pg_checkpointer', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f9..0cb5675a33f 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -42,9 +42,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
-#ifdef CATALOG_VARLEN			/* variable-length fields start here */
-	text		rolpassword;	/* password, if any */
-	timestamptz rolvaliduntil;	/* password expiration time, if any */
+#ifdef CATALOG_VARLEN
+	timestamptz rolvaliduntil BKI_FORCE_NULL;	/* role expiration time, if any */
 #endif
 } FormData_pg_authid;
 
@@ -55,10 +54,6 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
  */
 typedef FormData_pg_authid *Form_pg_authid;
 
-DECLARE_TOAST(pg_authid, 4175, 4176);
-#define PgAuthidToastTable 4175
-#define PgAuthidToastIndex 4176
-
 DECLARE_UNIQUE_INDEX(pg_authid_rolname_index, 2676, AuthIdRolnameIndexId, on pg_authid using btree(rolname name_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_authid_oid_index, 2677, AuthIdOidIndexId, on pg_authid using btree(oid oid_ops));
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65fd..ce39db5a491 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,6 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
+extern bool is_role_valid(const char *rolename, char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 3238cf66d3a..b8ff8ccb417 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,7 +21,7 @@
  * 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
+ * in pg_auth_password.password. They are also the allowed values for the
  * password_encryption GUC.
  */
 typedef enum PasswordType
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 9c1a76e8bb6..ab846e8b7fa 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,6 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
+	AUTHPASSWORD,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d55aec3a1d0..9875b277f26 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2515,10 +2515,10 @@ REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
 ERROR:  cannot reindex system catalogs concurrently
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 ERROR:  cannot reindex system catalogs concurrently
@@ -2817,10 +2817,10 @@ ERROR:  must be owner of schema schema_to_reindex
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
-REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+REINDEX TABLE pg_toast.pg_toast_1262;
+ERROR:  must be owner of table pg_toast_1262
+REINDEX INDEX pg_toast.pg_toast_1262_index;
+ERROR:  must be owner of index pg_toast_1262_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..b69a2a72a7b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_password {roleid} => pg_authid {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 7c84c9da337..4ffc41a5455 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -24,10 +24,12 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
@@ -40,12 +42,14 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
-       rolname       | rolpassword 
----------------------+-------------
+    ORDER BY rolname, password;
+       rolname       | password 
+---------------------+----------
  regress_passwd2_new | 
 (1 row)
 
@@ -72,10 +76,12 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
@@ -95,9 +101,11 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
- rolpassword 
--------------
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
+ password 
+----------
  
 (1 row)
 
@@ -110,8 +118,10 @@ CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
          rolname         | is_rolpassword_rehashed 
@@ -134,11 +144,13 @@ DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
- rolname | rolpassword 
----------+-------------
+    ORDER BY rolname, password;
+ rolname | password 
+---------+----------
 (0 rows)
 
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173e..18a9dacadde 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -1,233 +1,233 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
-           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
-          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
-           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 -- clean up roles
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index ac468568a1a..353917c9f81 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1710,11 +1710,12 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolsuper AS usesuper,
     pg_authid.rolreplication AS userepl,
     pg_authid.rolbypassrls AS usebypassrls,
-    pg_authid.rolpassword AS passwd,
+    p.password AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
     s.setconfig AS useconfig
-   FROM (pg_authid
+   FROM ((pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
+     LEFT JOIN pg_auth_password p ON ((p.roleid = pg_authid.oid)))
   WHERE pg_authid.rolcanlogin;
 pg_shmem_allocations| SELECT pg_get_shmem_allocations.name,
     pg_get_shmem_allocations.off,
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index 2dfbcfdebe1..f013c931087 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -44,13 +44,13 @@ ERROR:  cannot move system relation "pg_authid_rolname_index"
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 ERROR:  cannot reindex system catalogs concurrently
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 ERROR:  cannot reindex system catalogs concurrently
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index d8fded3d930..8d649f2355f 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -1068,9 +1068,9 @@ REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 COMMIT;
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 -- Warns about catalog relations
 REINDEX SCHEMA CONCURRENTLY pg_catalog;
@@ -1243,8 +1243,8 @@ REINDEX SCHEMA schema_to_reindex;
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-REINDEX INDEX pg_toast.pg_toast_1260_index;
+REINDEX TABLE pg_toast.pg_toast_1262;
+REINDEX INDEX pg_toast.pg_toast_1262_index;
 
 -- Clean up
 RESET ROLE;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 98f49916e5d..bb0c815121f 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -23,18 +23,22 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- Rename a role
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 
 -- Change passwords with ALTER USER. With plaintext or already-encrypted
@@ -63,16 +67,20 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
 ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
 
 -- Test with invalid stored and server keys.
 --
@@ -84,8 +92,10 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
 
@@ -103,7 +113,9 @@ DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d7303..09505c6a3be 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -1,83 +1,83 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
 
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 
 -- clean up roles
 DROP ROLE regress_test_def_superuser;
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index 896f05cea32..24208c20c3d 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -38,10 +38,10 @@ REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am;
 REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid;
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
 REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid;
-- 
2.31.1

#4Joshua Brindle
joshua.brindle@crunchydata.com
In reply to: Joshua Brindle (#3)
3 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Wed, Mar 2, 2022 at 9:58 AM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:

This is not intended for PG15.

Attached are a proof of concept patchset to implement multiple valid
passwords, which have independent expirations, set by a GUC or SQL
using an interval.

<snip>

postgres=# select * from pg_auth_password ;
roleid | name |
password
| expiration
--------+---------+-------------------------------------------------------------------------------------------------------------------
--------------------+-------------------------------
10 | __def__ |
SCRAM-SHA-256$4096:yGiHIYPwc2az7xj/7TIyTA==$OQL/AEcEY1yOCNbrZEj4zDvNnOLpIqltOW1uQvosLvc=:9VRRppuIkSrwhiBN5ePy8wB1y
zDa/2uX0WUx6gXi93E= |
16384 | __def__ |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$1Ivp4d+vAWxowpuGEn05KR9lxyGOms3yy85k3D7XpBg=:k8xUjU6xrJG17PMGa/Zya6pAE
/M7pEDaoIFmWvNIEUg= | 2022-03-02 06:52:31.217193-08
16384 | newone |
SCRAM-SHA-256$4096:AAAAAAAAAAAAAAAAAAAAAA==$WK3+41CCGDognSnZrtpHhv00z9LuVUjHR1hWq8T1+iE=:w2C5GuhgiEB7wXqPxYfxBKB+e
hm4h6Oeif1uzpPIFVk= | 2022-03-03 06:47:53.728159-08
(3 rows)

There's obviously a salt problem here that I'll need to fix that
apparently snuck in at the last rebase, but this brings up one aspect
of the patchset I didn't mention in the original email:

Attached are fixed patches rebased against the lastest master.

For the SCRAM protocol to work as is with existing clients the salt
for each password must be the same. Right now ALTER USER will find and
reuse the salt, but a user passing in a pre-computed SCRAM secret
currently has no way to get the salt.

for \password (we'll need a new one that takes a password name) I was
thinking libpq could hold onto the salt that was used to log in, but
for outside computation we'll need some way for the client to request
it.

None of that is done yet.

Now that the commitfest is over these are rebased on master.

It's unclear if I will be able to continue working on this featureset,
this email address will be inactive after today.

Attachments:

v2-0003-Per-password-expiration.patchapplication/x-patch; name=v2-0003-Per-password-expiration.patchDownload
From 6a52b2c16e99e2d9c734525cc0b75a3561e0e2c7 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Wed, 2 Mar 2022 06:07:40 -0800
Subject: [PATCH v2 3/3] Per-password expiration

To build on the multi-password support this
adds per-password expiration, either passed in via ALTER ROLE/CREATE
ROLE with the grammar EXPIRES IN, or via a system-wide setting
called password_valid_duration

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 121 ++++++++++++++++++++++++-
 src/backend/commands/variable.c        | 105 +++++++++++++++++++++
 src/backend/libpq/auth.c               |   9 +-
 src/backend/libpq/crypt.c              |  38 +++++++-
 src/backend/parser/gram.y              |  11 ++-
 src/backend/utils/misc/guc.c           |  13 +++
 src/include/catalog/pg_auth_password.h |   1 +
 src/include/commands/user.h            |   3 +
 src/include/commands/variable.h        |   4 +
 src/include/parser/kwlist.h            |   1 +
 10 files changed, 292 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 4117e58840a..e13f984ae09 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -48,8 +48,9 @@
 Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 
 
-/* GUC parameter */
+/* GUC parameters */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+Interval *default_password_duration = NULL;
 
 /* default password name */
 const char* default_passname = "__def__";
@@ -167,6 +168,84 @@ validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
 	return true;
 }
 
+static
+Datum
+expires_in_datum(DefElem *passExpiresIn)
+{
+	Interval *interval;
+	Node	   *arg;
+	A_Const    *con;
+	TimestampTz now = GetCurrentTimestamp();
+	Datum		passExpiresIn_datum;
+	char *dateout;
+
+	if (default_password_duration != NULL) 
+	{
+		/* The default duration GUC is set, use it if nothing came from SQL
+		 * or if something came from SQL, reject it if not from a superuser
+		 */
+
+		if (passExpiresIn != NULL)
+			if (!superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("must be superuser to override password_validity_duration GUC")));
+			else
+				goto bypass;
+		else
+		{
+			passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										 PointerGetDatum(default_password_duration));
+
+			dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+			ereport(NOTICE,
+				(errmsg("Password will expire at: \"%s\" (from GUC)", dateout)));
+
+			return passExpiresIn_datum;
+		}
+	}
+
+	if (passExpiresIn == NULL) 	/* No duration requested via SQL and no system default, no expiration */
+		return PointerGetDatum(NULL);
+
+bypass:
+ 	arg = (Node *)passExpiresIn->arg;
+	if (IsA(arg, TypeCast))
+	{
+		TypeCast   *tc = (TypeCast *) passExpiresIn->arg;
+		arg = tc->arg;
+	}
+
+	if (!IsA(arg, A_Const))
+	{
+		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
+		return PointerGetDatum(NULL);
+	}
+	con = (A_Const *) arg;
+
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+								CStringGetDatum(strVal(&con->val)),
+								ObjectIdGetDatum(InvalidOid),
+								Int32GetDatum(-1)));
+
+	passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										PointerGetDatum(interval));
+
+	dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+	ereport(NOTICE,
+		(errmsg("Password will expire at: \"%s\" (from SQL)", dateout)));
+
+	return passExpiresIn_datum;
+}
+
 /*
  * CREATE ROLE
  */
@@ -211,7 +290,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
-
+	DefElem    *dpassExpiresIn = NULL;
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -320,6 +399,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 
 		else
 			elog(ERROR, "option \"%s\" not recognized",
@@ -508,6 +593,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
 		HeapTuple	new_tuple;
+		Datum		passExpiresIn_datum;
 
 		/*
 		 * Don't allow an empty password. Libpq treats an empty password the
@@ -529,19 +615,27 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+			if (passExpiresIn_datum != PointerGetDatum(NULL))
+				new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+			else
+				new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+	
 			/* Encrypt the password to the requested format. */
 			validate_and_get_salt(stmt->role, &salt, &logdetail);
 			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
-			MemSet(new_password_record, 0, sizeof(new_password_record));
-			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 			if (passname != NULL)
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(passname));
 			else
 				new_password_record[Anum_pg_auth_password_name - 1] = 
 								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -648,6 +742,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
 	bool		validUntil_null;
+	Datum		passExpiresIn_datum; /* Time period until password expires */
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
@@ -660,6 +755,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
+	DefElem	   *dpassExpiresIn = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -743,6 +839,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -763,6 +865,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (dpassName)
 		passname = strVal(dpassName->arg);
 
+
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
 	 */
@@ -849,6 +952,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
 
+	passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+	if (passExpiresIn_datum != PointerGetDatum(NULL)) {
+		new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+		new_password_record_repl[Anum_pg_auth_password_expiration - 1] = true;
+	}
+	else
+		new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
 
 	/*
 	 * issuper/createrole/etc
@@ -958,7 +1068,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
-	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true 
+		|| new_password_record_repl[Anum_pg_auth_password_expiration - 1] == true)
 	{
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4a..ca85a10b80b 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "commands/user.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -933,3 +934,107 @@ show_role(void)
 	/* Otherwise we can just use the GUC string */
 	return role_string ? role_string : "none";
 }
+
+
+/*
+ * check_password_duration: GUC check_hook for password_duration
+ */
+bool
+check_password_duration(char **newval, void **extra, GucSource source)
+{
+	Interval	*new_interval;
+	char	   *endptr;
+
+	const char *valueptr = *newval;
+	char	   *val;
+	Interval   *interval;
+
+	if (newval == NULL || *newval == NULL) {
+		extra = NULL;
+		return true;
+	}
+
+	elog(NOTICE,"Setting password duration to \"%s\"",
+					*newval);
+
+	while (isspace((unsigned char) *valueptr))
+		valueptr++;
+	if (*valueptr != '\'') {
+		val = pstrdup(valueptr);
+	}
+	else
+	{
+		valueptr++;
+		val = pstrdup(valueptr);
+		/* Check and remove trailing quote */
+		endptr = strchr(val, '\'');
+		if (!endptr || endptr[1] != '\0')
+		{
+			pfree(val);
+			return false;
+		}
+		*endptr = '\0';
+	}
+
+	/*
+		* Try to parse it.  XXX an invalid interval format will result in
+		* ereport(ERROR), which is not desirable for GUC.  We did what we
+		* could to guard against this in flatten_set_variable_args, but a
+		* string coming in from postgresql.conf might contain anything.
+		*/
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+														CStringGetDatum(val),
+														ObjectIdGetDatum(InvalidOid),
+														Int32GetDatum(-1)));
+
+	pfree(val);
+
+	if (!interval) {
+		return false;
+	}
+
+	new_interval = malloc(sizeof(Interval));
+	memcpy(new_interval, interval, sizeof(Interval));
+	pfree(interval);
+
+	/*
+	 * Pass back data for assign_password_validity to use
+	 */
+	*extra = malloc(sizeof(Interval *));
+	if (!*extra)
+		return false;
+	*((Interval **) *extra) = new_interval;
+
+	return true;
+}
+
+/*
+ * assign_password_validity: GUC assign_hook for timezone
+ */
+void
+assign_password_duration(const char *newval, void *extra)
+{
+	if (extra == NULL)
+		default_password_duration = NULL;
+	else
+		default_password_duration = *((Interval **) extra);
+}
+
+/*
+ * show_password_validity: GUC show_hook for timezone
+ */
+const char *
+show_password_duration(void)
+{
+	const char *intervalout;
+	if (default_password_duration == NULL) {
+		return "";
+	}
+	intervalout = DatumGetCString(DirectFunctionCall1(interval_out,
+										PointerGetDatum(default_password_duration)));
+
+	if (intervalout != NULL)
+		return intervalout;
+
+	return "";
+}
\ No newline at end of file
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5569599ab31..641cc92549b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -855,9 +855,14 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
 											logdetail);
 
-		for (i = 0; i < num; i++) 
-			pfree(passwords[i]);
+		for (i = 0; i < num; i++) {
 
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(LOG,
+					(errmsg("Password %d was null", i)));
+		}
 		pfree(passwords);
 	}
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 09b6bebf40c..8efb73b6c29 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -40,10 +40,12 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
+	TimestampTz current, vuntil = 0;
+
 	bool		isnull;
 	char	   **passwords;
 	CatCList   *passlist;
-	int		    i;
+	int		    i, j = 0, num_valid_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -51,6 +53,7 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	{
 		*logdetail = psprintf(_("Role \"%s\" does not exist."),
 							  role);
+		*num = 0;
 		return NULL;			/* no such user */
 	}
 
@@ -58,7 +61,6 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	ReleaseSysCache(roleTup);
 	/* Find any existing password that is not the one being updated to get the salt */
 	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
-	*num = passlist->n_members;
 
 	if (passlist->n_members == 0)
 	{
@@ -69,15 +71,41 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 		return NULL;			/* user has no password */
 	}
 
-	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+	current = GetCurrentTimestamp();
 
 	for (i = 0; i < passlist->n_members; i++)
 	{
 		HeapTuple	tup = &passlist->members[i]->tuple;
 
 		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
-							Anum_pg_auth_password_password, &isnull);
-		passwords[i] = TextDatumGetCString(datum);
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+			num_valid_passwords++;
+	}
+
+	passwords = palloc0(sizeof(char *) * num_valid_passwords); 
+	*num = num_valid_passwords;
+	
+	for (i = 0; i < passlist->n_members; i++)
+	{
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+		{
+			datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+								Anum_pg_auth_password_password, &isnull);
+			passwords[j++] = pstrdup(TextDatumGetCString(datum));
+		}
 	}
 
 	ReleaseCatCacheList(passlist);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3cb6c76aa5..0121cc79ee6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -790,7 +790,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DOUBLE_P DROP
 
 	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
-	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPIRES EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -826,7 +826,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD 
 	PATH PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
@@ -1262,6 +1261,12 @@ AlterOptRoleElem:
 					$$ = makeDefElem("passname",
 									 (Node *)makeString($2), @1);
 				}
+			| EXPIRES IN_P Sconst opt_interval
+				{
+					TypeName *t = SystemTypeName("interval");
+					t->typmods = $4;
+					$$ = makeDefElem("expiresin", (Node *)makeStringConstCast($3, @3, t), @1);
+				}
 			| VALID FOR Sconst
 				{
 					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
@@ -16986,6 +16991,7 @@ unreserved_keyword:
 			| EXCLUDING
 			| EXCLUSIVE
 			| EXECUTE
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -17560,6 +17566,7 @@ bare_label_keyword:
 			| EXCLUSIVE
 			| EXECUTE
 			| EXISTS
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 22b5571a704..81208c00ea1 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -716,6 +716,7 @@ static char *recovery_target_string;
 static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
+static char *password_duration_string;
 
 
 /* should be static, but commands/variable.c needs to get at this */
@@ -4721,6 +4722,17 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"password_valid_duration", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Specifies the default validity duration of new passwords."),
+			NULL,
+			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
+		},
+		&password_duration_string,
+		NULL,
+		check_password_duration, assign_password_duration, show_password_duration
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
@@ -5119,6 +5131,7 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index 4181caad30b..1cb0a211ee7 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -31,6 +31,7 @@ CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_RO
 
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
     text            password BKI_FORCE_NOT_NULL;        /* password */
+    timestamptz     expiration BKI_FORCE_NULL;	        /* password expiration time, if any */
 #endif
 } FormData_pg_auth_password;
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 1c14f60a05b..13d3479facb 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -19,6 +19,9 @@
 /* GUC. Is actually of type PasswordType. */
 extern PGDLLIMPORT int Password_encryption;
 
+/* GUC. system-wide password validity duration */
+extern Interval *default_password_duration;
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/commands/variable.h b/src/include/commands/variable.h
index 0e5ddcbcf37..3b941c554cb 100644
--- a/src/include/commands/variable.h
+++ b/src/include/commands/variable.h
@@ -34,5 +34,9 @@ extern void assign_session_authorization(const char *newval, void *extra);
 extern bool check_role(char **newval, void **extra, GucSource source);
 extern void assign_role(const char *newval, void *extra);
 extern const char *show_role(void);
+extern bool check_password_duration(char **newval, void **extra, GucSource source);
+extern void assign_password_duration(const char *newval, void *extra);
+extern const char *show_password_duration(void);
+
 
 #endif							/* VARIABLE_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bf84593a61c..616ffe38fcb 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -163,6 +163,7 @@ PG_KEYWORD("excluding", EXCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exclusive", EXCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("execute", EXECUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exists", EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("expires", EXPIRES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("explain", EXPLAIN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("expression", EXPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("extension", EXTENSION, UNRESERVED_KEYWORD, BARE_LABEL)
-- 
2.35.1

v2-0001-Move-rolpassword-out-of-pg_authid-into-a-new-tabl.patchapplication/x-patch; name=v2-0001-Move-rolpassword-out-of-pg_authid-into-a-new-tabl.patchDownload
From 1d87319a60a1ed14ec6ea5c2eea80d95e70433bb Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Thu, 9 Dec 2021 16:18:25 -0800
Subject: [PATCH v2 1/3] Move rolpassword out of pg_authid into a new table

In preparation for more flexibility in password
management the rolpassword column needs to be
moved into a new table.

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/catalog/Makefile                 |   2 +-
 src/backend/catalog/catalog.c                |   8 +-
 src/backend/catalog/system_views.sql         |   5 +-
 src/backend/commands/user.c                  | 226 ++++++++++++----
 src/backend/libpq/auth-sasl.c                |   2 +-
 src/backend/libpq/auth-scram.c               |   4 +-
 src/backend/libpq/auth.c                     |  16 ++
 src/backend/libpq/crypt.c                    |  40 ++-
 src/backend/utils/cache/catcache.c           |   1 +
 src/backend/utils/cache/relcache.c           |  13 +-
 src/backend/utils/cache/syscache.c           |  12 +
 src/bin/initdb/initdb.c                      |   2 +-
 src/bin/pg_dump/pg_dumpall.c                 |  13 +-
 src/common/scram-common.c                    |   2 +-
 src/include/catalog/pg_auth_password.h       |  50 ++++
 src/include/catalog/pg_authid.dat            |  26 +-
 src/include/catalog/pg_authid.h              |   9 +-
 src/include/commands/user.h                  |   1 +
 src/include/libpq/crypt.h                    |   2 +-
 src/include/utils/syscache.h                 |   1 +
 src/test/regress/expected/create_index.out   |  14 +-
 src/test/regress/expected/oidjoins.out       |   1 +
 src/test/regress/expected/password.out       |  44 ++--
 src/test/regress/expected/roleattributes.out | 256 +++++++++----------
 src/test/regress/expected/rules.out          |   5 +-
 src/test/regress/expected/tablespace.out     |  12 +-
 src/test/regress/sql/create_index.sql        |  10 +-
 src/test/regress/sql/password.sql            |  32 ++-
 src/test/regress/sql/roleattributes.sql      |  64 ++---
 src/test/regress/sql/tablespace.sql          |   8 +-
 30 files changed, 563 insertions(+), 318 deletions(-)
 create mode 100644 src/include/catalog/pg_auth_password.h

diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9b..450cfd9a589 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -63,7 +63,7 @@ CATALOG_HEADERS := \
 	pg_rewrite.h pg_trigger.h pg_event_trigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h \
-	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
+	pg_authid.h pg_auth_members.h pg_auth_password.h pg_shdepend.h pg_shdescription.h \
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 520f77971b3..11c149fd625 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -28,6 +28,7 @@
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
@@ -246,6 +247,7 @@ IsSharedRelation(Oid relationId)
 {
 	/* These are the shared catalogs (look for BKI_SHARED_RELATION) */
 	if (relationId == AuthIdRelationId ||
+		relationId == AuthPasswordRelationId ||
 		relationId == AuthMemRelationId ||
 		relationId == DatabaseRelationId ||
 		relationId == DbRoleSettingRelationId ||
@@ -260,6 +262,8 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
+		relationId == AuthPasswordRoleOidIndexId ||
+		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
 		relationId == DatabaseNameIndexId ||
@@ -279,8 +283,8 @@ IsSharedRelation(Oid relationId)
 		relationId == TablespaceOidIndexId)
 		return true;
 	/* These are their toast tables and toast indexes */
-	if (relationId == PgAuthidToastTable ||
-		relationId == PgAuthidToastIndex ||
+	if (relationId == PgAuthPasswordToastTable ||
+		relationId == PgAuthPasswordToastIndex ||
 		relationId == PgDatabaseToastTable ||
 		relationId == PgDatabaseToastIndex ||
 		relationId == PgDbRoleSettingToastTable ||
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0fc614e32c0..c1e42e6716f 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -40,11 +40,14 @@ CREATE VIEW pg_shadow AS
         rolsuper AS usesuper,
         rolreplication AS userepl,
         rolbypassrls AS usebypassrls,
-        rolpassword AS passwd,
+        p.password AS passwd,
         rolvaliduntil AS valuntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
+    LEFT JOIN pg_auth_password p
+    ON p.roleid = pg_authid.oid
+
     WHERE rolcanlogin;
 
 REVOKE ALL ON pg_shadow FROM public;
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index c263f6c8b9f..5b6ce37f1bf 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -23,6 +23,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "commands/comment.h"
@@ -64,6 +65,42 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+/*
+ * Is the role able to log in
+*/
+bool
+is_role_valid(const char *rolename, char **logdetail)
+{
+	HeapTuple	roleTup;
+	Datum		datum;
+	bool		isnull;
+	TimestampTz vuntil = 0;
+
+	/* Get role info from pg_authid */
+	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(rolename));
+	if (!HeapTupleIsValid(roleTup))
+	{
+		*logdetail = psprintf(_("Role \"%s\" does not exist."),
+							  rolename);
+		return false;			/* no such user */
+	}
+
+	datum = SysCacheGetAttr(AUTHNAME, roleTup,
+							Anum_pg_authid_rolvaliduntil, &isnull);
+	ReleaseSysCache(roleTup);
+
+	if (!isnull)
+		vuntil = DatumGetTimestampTz(datum);
+
+	if (!isnull && vuntil < GetCurrentTimestamp())
+	{
+		*logdetail = psprintf(_("User \"%s\" has an expired password."), rolename);
+		return false;
+	}
+
+	return true;
+}
+
 
 /*
  * CREATE ROLE
@@ -351,43 +388,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
 	new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
 	new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
-
-	if (password)
-	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
-
-		/*
-		 * Don't allow an empty password. Libpq treats an empty password the
-		 * same as no password at all, and won't even try to authenticate. But
-		 * other clients might, so allowing it would be confusing. By clearing
-		 * the password when an empty string is specified, the account is
-		 * consistently locked for all clients.
-		 *
-		 * Note that this only covers passwords stored in the database itself.
-		 * There are also checks in the authentication code, to forbid an
-		 * empty password from being used with authentication methods that
-		 * fetch the password from an external system, like LDAP or PAM.
-		 */
-		if (password[0] == '\0' ||
-			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
-		{
-			ereport(NOTICE,
-					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-		}
-		else
-		{
-			/* 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);
-		}
-	}
-	else
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
@@ -422,6 +422,60 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	CatalogTupleInsert(pg_authid_rel, tuple);
 
+	if (password)
+	{
+		char	   *shadow_pass;
+		char	   *logdetail;
+		Datum		new_password_record[Natts_pg_auth_password];
+		bool		new_password_record_nulls[Natts_pg_auth_password];
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple	new_tuple;
+
+		/*
+		 * Don't allow an empty password. Libpq treats an empty password the
+		 * same as no password at all, and won't even try to authenticate. But
+		 * other clients might, so allowing it would be confusing. By clearing
+		 * the password when an empty string is specified, the account is
+		 * consistently locked for all clients.
+		 *
+		 * Note that this only covers passwords stored in the database itself.
+		 * There are also checks in the authentication code, to forbid an
+		 * empty password from being used with authentication methods that
+		 * fetch the password from an external system, like LDAP or PAM.
+		 */
+		if (password[0] == '\0' ||
+			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+		}
+		else
+		{
+			/* Encrypt the password to the requested format. */
+			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+										   password);
+
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			/* open password table and insert it. */
+			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+
+			new_password_record[Anum_pg_auth_password_password - 1] =
+				CStringGetTextDatum(shadow_pass);
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+			table_close(pg_auth_password_rel, NoLock);
+
+		}
+	}
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -495,6 +549,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	Datum		new_record[Natts_pg_authid];
 	bool		new_record_nulls[Natts_pg_authid];
 	bool		new_record_repl[Natts_pg_authid];
+	Datum		new_password_record[Natts_pg_auth_password];
+	bool		new_password_record_nulls[Natts_pg_auth_password];
+	bool		new_password_record_repl[Natts_pg_auth_password];
 	Relation	pg_authid_rel;
 	TupleDesc	pg_authid_dsc;
 	HeapTuple	tuple,
@@ -624,6 +681,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	rolename = pstrdup(NameStr(authform->rolname));
 	roleid = authform->oid;
 
+
 	/*
 	 * To mess with a superuser or replication role in any way you gotta be
 	 * superuser.  We also insist on superuser to change the BYPASSRLS
@@ -694,6 +752,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_record, 0, sizeof(new_record));
 	MemSet(new_record_nulls, false, sizeof(new_record_nulls));
 	MemSet(new_record_repl, false, sizeof(new_record_repl));
+	MemSet(new_password_record, 0, sizeof(new_password_record));
+	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
+
 
 	/*
 	 * issuper/createrole/etc
@@ -752,24 +814,24 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		{
 			ereport(NOTICE,
 					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+			new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 		}
 		else
 		{
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, rolename,
 										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
+			new_password_record[Anum_pg_auth_password_password - 1] =
 				CStringGetTextDatum(shadow_pass);
 		}
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
 	if (dpassword && dpassword->arg == NULL)
 	{
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+		new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* valid until */
@@ -786,12 +848,48 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
 								  new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
-
-	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
-
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	{
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple   password_tuple;
+
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+
+		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
+		{
+			if (HeapTupleIsValid(password_tuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &password_tuple->t_self);
+				ReleaseSysCache(password_tuple);
+			}
+		}
+		else if (HeapTupleIsValid(password_tuple)) /* update existing password */
+		{
+			new_tuple = heap_modify_tuple(password_tuple, pg_auth_password_dsc, new_password_record,
+										new_password_record_nulls, new_password_record_repl);
+			CatalogTupleUpdate(pg_auth_password_rel, &password_tuple->t_self, new_tuple);
+			ReleaseSysCache(password_tuple);
+			heap_freetuple(new_tuple);
+		}
+		else /* insert new password */
+		{
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+		}
+
+		table_close(pg_auth_password_rel, NoLock);
+	}
+	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -901,7 +999,6 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 	return roleid;
 }
 
-
 /*
  * DROP ROLE
  */
@@ -909,7 +1006,8 @@ void
 DropRole(DropRoleStmt *stmt)
 {
 	Relation	pg_authid_rel,
-				pg_auth_members_rel;
+				pg_auth_members_rel,
+				pg_auth_password_rel;
 	ListCell   *item;
 
 	if (!have_createrole_privilege())
@@ -923,6 +1021,8 @@ DropRole(DropRoleStmt *stmt)
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+
 
 	foreach(item, stmt->roles)
 	{
@@ -1056,6 +1156,15 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedComments(roleid, AuthIdRelationId);
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
+		/*
+		 * Drop password
+		 */
+		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
+		if (HeapTupleIsValid(tuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
+			ReleaseSysCache(tuple);
+		}
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1077,7 +1186,9 @@ DropRole(DropRoleStmt *stmt)
 	 * Now we can clean up; but keep locks until commit.
 	 */
 	table_close(pg_auth_members_rel, NoLock);
+	table_close(pg_auth_password_rel, NoLock);
 	table_close(pg_authid_rel, NoLock);
+
 }
 
 /*
@@ -1087,11 +1198,12 @@ ObjectAddress
 RenameRole(const char *oldname, const char *newname)
 {
 	HeapTuple	oldtuple,
-				newtuple;
+				newtuple,
+				passtuple;
 	TupleDesc	dsc;
 	Relation	rel;
 	Datum		datum;
-	bool		isnull;
+	bool		isnull = true;
 	Datum		repl_val[Natts_pg_authid];
 	bool		repl_null[Natts_pg_authid];
 	bool		repl_repl[Natts_pg_authid];
@@ -1189,14 +1301,24 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
+	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+
+	if (HeapTupleIsValid(passtuple))
+		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
+								Anum_pg_auth_password_password, &isnull);
 
 	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
 	{
+		Relation	pg_auth_password_rel;
+
 		/* MD5 uses the username as salt, so just clear it on a rename */
-		repl_repl[Anum_pg_authid_rolpassword - 1] = true;
-		repl_null[Anum_pg_authid_rolpassword - 1] = true;
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
+		if (HeapTupleIsValid(passtuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+			ReleaseSysCache(passtuple);
+		}
+		table_close(pg_auth_password_rel, NoLock);
 		ereport(NOTICE,
 				(errmsg("MD5 password cleared because of role rename")));
 	}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index a1d7dbb6d58..805b3695b78 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -33,7 +33,7 @@
  * implementation.
  *
  * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
+ * authenticated, from pg_auth_password.password.  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.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218ab..795f1cba555 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -220,7 +220,7 @@ 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.
+ * 'shadow_pass' is the role's stored secret, from pg_auth_password.password.
  * 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
@@ -454,7 +454,7 @@ 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_auth_password.password.
  *
  * The result is palloc'd, so caller is responsible for freeing it.
  */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f31353..5cfde1eaa13 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -592,10 +592,26 @@ ClientAuthentication(Port *port)
 
 		case uaMD5:
 		case uaSCRAM:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPWChallengeAuth(port, &logdetail);
 			break;
 
 		case uaPassword:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+	 		 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPasswordAuth(port, &logdetail);
 			break;
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 1ff8b0507d4..745e61034c2 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_auth_password.password.
  *
  * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -16,6 +16,7 @@
 #include <unistd.h>
 
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "common/md5.h"
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
@@ -36,8 +37,7 @@
 char *
 get_role_password(const char *role, const char **logdetail)
 {
-	TimestampTz vuntil = 0;
-	HeapTuple	roleTup;
+	HeapTuple	roleTup, passTup;
 	Datum		datum;
 	bool		isnull;
 	char	   *shadow_pass;
@@ -51,33 +51,29 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;			/* no such user */
 	}
 
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
+	
+	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+
+	if (!HeapTupleIsValid(passTup))
 	{
-		ReleaseSysCache(roleTup);
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
-
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
-	if (!isnull)
-		vuntil = DatumGetTimestampTz(datum);
+	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
+							Anum_pg_auth_password_password, &isnull);
 
 	ReleaseSysCache(roleTup);
-
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (isnull) /* this should not happen any more but just in case */
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
+		ReleaseSysCache(passTup);
+		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
-		return NULL;
+		return NULL;			/* user has no password */
 	}
+	shadow_pass = TextDatumGetCString(datum);
+	ReleaseSysCache(passTup);
 
 	return shadow_pass;
 }
@@ -156,7 +152,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_auth_password.password.
  * '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.
  *
@@ -211,7 +207,7 @@ 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_auth_password.password.
  * '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/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index ec073e1ed06..356e9cde6fb 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,6 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
+		case AUTHPASSWORD:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43f14c233d6..d84cb883f51 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_namespace.h"
@@ -113,6 +114,7 @@ static const FormData_pg_attribute Desc_pg_proc[Natts_pg_proc] = {Schema_pg_proc
 static const FormData_pg_attribute Desc_pg_type[Natts_pg_type] = {Schema_pg_type};
 static const FormData_pg_attribute Desc_pg_database[Natts_pg_database] = {Schema_pg_database};
 static const FormData_pg_attribute Desc_pg_authid[Natts_pg_authid] = {Schema_pg_authid};
+static const FormData_pg_attribute Desc_pg_auth_password[Natts_pg_auth_password] = {Schema_pg_auth_password};
 static const FormData_pg_attribute Desc_pg_auth_members[Natts_pg_auth_members] = {Schema_pg_auth_members};
 static const FormData_pg_attribute Desc_pg_index[Natts_pg_index] = {Schema_pg_index};
 static const FormData_pg_attribute Desc_pg_shseclabel[Natts_pg_shseclabel] = {Schema_pg_shseclabel};
@@ -3485,6 +3487,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case DatabaseRelationId:
 		case AuthIdRelationId:
+		case AuthPasswordRelationId:
 		case AuthMemRelationId:
 		case RelationRelationId:
 		case AttributeRelationId:
@@ -3905,7 +3908,7 @@ RelationCacheInitialize(void)
  *		RelationCacheInitializePhase2
  *
  *		This is called to prepare for access to shared catalogs during startup.
- *		We must at least set up nailed reldescs for pg_database, pg_authid,
+ *		We must at least set up nailed reldescs for pg_database, pg_authid, pg_auth_password,
  *		pg_auth_members, and pg_shseclabel. Ideally we'd like to have reldescs
  *		for their indexes, too.  We attempt to load this information from the
  *		shared relcache init file.  If that's missing or broken, just make
@@ -3950,8 +3953,10 @@ RelationCacheInitializePhase2(void)
 				  Natts_pg_shseclabel, Desc_pg_shseclabel);
 		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
 				  Natts_pg_subscription, Desc_pg_subscription);
+		formrdesc("pg_auth_password", AuthPasswordRelation_Rowtype_Id, true,
+				  Natts_pg_auth_password, Desc_pg_auth_password);
 
-#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_RELS	6	/* fix if you change list above */
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -4090,8 +4095,10 @@ RelationCacheInitializePhase3(void)
 							AuthMemRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
+		load_critical_index(AuthPasswordRoleOidIndexId,
+							AuthPasswordRelationId);
 
-#define NUM_CRITICAL_SHARED_INDEXES 6	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_INDEXES 7	/* fix if you change list above */
 
 		criticalSharedRelcachesBuilt = true;
 	}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b121463..de2b3ec1ebf 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -255,6 +256,17 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+		AuthPasswordRoleOidIndexId,
+		1,
+		{
+			Anum_pg_auth_password_roleid,
+			0,
+			0,
+			0
+		},
+		8
+	},
 	{
 		CastRelationId,			/* CASTSOURCETARGET */
 		CastSourceTargetIndexId,
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index bbca1236d95..16349dc0510 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1452,7 +1452,7 @@ setup_auth(FILE *cmdfd)
 		 * The authid table shouldn't be readable except through views, to
 		 * ensure passwords are not publicly visible.
 		 */
-		"REVOKE ALL ON pg_authid FROM public;\n\n",
+		"REVOKE ALL ON pg_auth_password FROM public;\n\n",
 		NULL
 	};
 
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 2dc33627630..fc282c8046d 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -759,7 +759,18 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* note: rolconfig is dumped later */
-	if (server_version >= 90600)
+	if (server_version >= 150000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, p.password as rolpassword, "
+						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s LEFT JOIN pg_auth_password p ON %s.oid = p.roleid "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog, role_catalog, role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index 12686259299..d9d0cba7379 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -181,7 +181,7 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result,
 
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_auth_password.password.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
new file mode 100644
index 00000000000..beaa2d40b90
--- /dev/null
+++ b/src/include/catalog/pg_auth_password.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_auth_password.h
+ *	  definition of the "authorization identifier" system catalog (pg_auth_password)
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_auth_password.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_AUTH_PASSWORD_H
+#define PG_AUTH_PASSWORD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_auth_password_d.h"
+
+/* ----------------
+ *		pg_auth_password definition.  cpp turns this into
+ *		typedef struct FormData_pg_auth_password
+ * ----------------
+ */
+CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+{
+	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+#ifdef CATALOG_VARLEN                /* variable-length fields start here */
+    text            password;        /* password */
+#endif
+} FormData_pg_auth_password;
+
+/* ----------------
+ *		Form_pg_auth_password corresponds to a pointer to a tuple with
+ *		the format of pg_auth_password relation.
+ * ----------------
+ */
+
+DECLARE_TOAST(pg_auth_password, 4175, 4176);
+#define PgAuthPasswordToastTable 4175
+#define PgAuthPasswordToastIndex 4176
+
+typedef FormData_pg_auth_password *Form_pg_auth_password;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
+
+
+#endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6c28119fa1a..da36375a582 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,66 +23,66 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_null_' },
 { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINTER',
   rolname => 'pg_checkpointer', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f9..0cb5675a33f 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -42,9 +42,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
-#ifdef CATALOG_VARLEN			/* variable-length fields start here */
-	text		rolpassword;	/* password, if any */
-	timestamptz rolvaliduntil;	/* password expiration time, if any */
+#ifdef CATALOG_VARLEN
+	timestamptz rolvaliduntil BKI_FORCE_NULL;	/* role expiration time, if any */
 #endif
 } FormData_pg_authid;
 
@@ -55,10 +54,6 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
  */
 typedef FormData_pg_authid *Form_pg_authid;
 
-DECLARE_TOAST(pg_authid, 4175, 4176);
-#define PgAuthidToastTable 4175
-#define PgAuthidToastIndex 4176
-
 DECLARE_UNIQUE_INDEX(pg_authid_rolname_index, 2676, AuthIdRolnameIndexId, on pg_authid using btree(rolname name_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_authid_oid_index, 2677, AuthIdOidIndexId, on pg_authid using btree(oid oid_ops));
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index d3dd8303d28..3e1aae57854 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,6 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
+extern bool is_role_valid(const char *rolename, char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 3238cf66d3a..b8ff8ccb417 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,7 +21,7 @@
  * 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
+ * in pg_auth_password.password. They are also the allowed values for the
  * password_encryption GUC.
  */
 typedef enum PasswordType
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 4463ea66bea..c7bc648f46a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,6 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
+	AUTHPASSWORD,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d55aec3a1d0..9875b277f26 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2515,10 +2515,10 @@ REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
 ERROR:  cannot reindex system catalogs concurrently
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 ERROR:  cannot reindex system catalogs concurrently
@@ -2817,10 +2817,10 @@ ERROR:  must be owner of schema schema_to_reindex
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
-REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+REINDEX TABLE pg_toast.pg_toast_1262;
+ERROR:  must be owner of table pg_toast_1262
+REINDEX INDEX pg_toast.pg_toast_1262_index;
+ERROR:  must be owner of index pg_toast_1262_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..b69a2a72a7b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_password {roleid} => pg_authid {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 7c84c9da337..4ffc41a5455 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -24,10 +24,12 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
@@ -40,12 +42,14 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
-       rolname       | rolpassword 
----------------------+-------------
+    ORDER BY rolname, password;
+       rolname       | password 
+---------------------+----------
  regress_passwd2_new | 
 (1 row)
 
@@ -72,10 +76,12 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
@@ -95,9 +101,11 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
- rolpassword 
--------------
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
+ password 
+----------
  
 (1 row)
 
@@ -110,8 +118,10 @@ CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
          rolname         | is_rolpassword_rehashed 
@@ -134,11 +144,13 @@ DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
- rolname | rolpassword 
----------+-------------
+    ORDER BY rolname, password;
+ rolname | password 
+---------+----------
 (0 rows)
 
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173e..18a9dacadde 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -1,233 +1,233 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
-           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
-          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
-           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 -- clean up roles
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f29375c2a90..56bc581ab35 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1716,11 +1716,12 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolsuper AS usesuper,
     pg_authid.rolreplication AS userepl,
     pg_authid.rolbypassrls AS usebypassrls,
-    pg_authid.rolpassword AS passwd,
+    p.password AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
     s.setconfig AS useconfig
-   FROM (pg_authid
+   FROM ((pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
+     LEFT JOIN pg_auth_password p ON ((p.roleid = pg_authid.oid)))
   WHERE pg_authid.rolcanlogin;
 pg_shmem_allocations| SELECT pg_get_shmem_allocations.name,
     pg_get_shmem_allocations.off,
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index c52cf1cfcf9..2ca2cd181f1 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -53,13 +53,13 @@ ERROR:  cannot move system relation "pg_authid_rolname_index"
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 ERROR:  cannot reindex system catalogs concurrently
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 ERROR:  cannot reindex system catalogs concurrently
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index d8fded3d930..8d649f2355f 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -1068,9 +1068,9 @@ REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 COMMIT;
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 -- Warns about catalog relations
 REINDEX SCHEMA CONCURRENTLY pg_catalog;
@@ -1243,8 +1243,8 @@ REINDEX SCHEMA schema_to_reindex;
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-REINDEX INDEX pg_toast.pg_toast_1260_index;
+REINDEX TABLE pg_toast.pg_toast_1262;
+REINDEX INDEX pg_toast.pg_toast_1262_index;
 
 -- Clean up
 RESET ROLE;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 98f49916e5d..bb0c815121f 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -23,18 +23,22 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- Rename a role
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 
 -- Change passwords with ALTER USER. With plaintext or already-encrypted
@@ -63,16 +67,20 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
 ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
 
 -- Test with invalid stored and server keys.
 --
@@ -84,8 +92,10 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
 
@@ -103,7 +113,9 @@ DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d7303..09505c6a3be 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -1,83 +1,83 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
 
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 
 -- clean up roles
 DROP ROLE regress_test_def_superuser;
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index 21db433f2a8..d92135173fb 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -42,10 +42,10 @@ REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am;
 REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid;
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
 REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid;
-- 
2.35.1

v2-0002-multiple-passwords-work-with-scram-and-md5.patchapplication/x-patch; name=v2-0002-multiple-passwords-work-with-scram-and-md5.patchDownload
From 446f4216b55db0b3ae1a90e46a9ef1983634e240 Mon Sep 17 00:00:00 2001
From: Joshua Brindle <joshua.brindle@crunchydata.com>
Date: Tue, 1 Feb 2022 14:32:55 -0800
Subject: [PATCH v2 2/3] multiple passwords work with scram and md5

also renaming roles, dropping roles, switching between
password_encryption

Signed-off-by: Joshua Brindle <joshua.brindle@crunchydata.com>
---
 src/backend/commands/user.c            | 199 +++++++++++++++++++++----
 src/backend/libpq/auth-sasl.c          |   4 +-
 src/backend/libpq/auth-scram.c         | 184 +++++++++++++----------
 src/backend/libpq/auth.c               |  94 +++++-------
 src/backend/libpq/crypt.c              |  51 ++++---
 src/backend/parser/gram.y              |  16 +-
 src/backend/utils/cache/catcache.c     |   2 +-
 src/backend/utils/cache/syscache.c     |   6 +-
 src/include/catalog/pg_auth_password.h |   9 +-
 src/include/commands/user.h            |   2 +-
 src/include/libpq/crypt.h              |   2 +-
 src/include/libpq/sasl.h               |   4 +-
 src/include/libpq/scram.h              |   2 +-
 src/include/parser/kwlist.h            |   1 +
 src/include/utils/syscache.h           |   2 +-
 src/test/regress/expected/password.out |   2 +-
 16 files changed, 376 insertions(+), 204 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b6ce37f1bf..4117e58840a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -18,6 +18,7 @@
 #include "access/xact.h"
 #include "catalog/binary_upgrade.h"
 #include "catalog/catalog.h"
+
 #include "catalog/dependency.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
@@ -31,11 +32,14 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/fmgroids.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -47,6 +51,9 @@ Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 /* GUC parameter */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
 
+/* default password name */
+const char* default_passname = "__def__";
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 check_password_hook_type check_password_hook = NULL;
 
@@ -69,7 +76,7 @@ have_createrole_privilege(void)
  * Is the role able to log in
 */
 bool
-is_role_valid(const char *rolename, char **logdetail)
+is_role_valid(const char *rolename, const char **logdetail)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
@@ -102,6 +109,64 @@ is_role_valid(const char *rolename, char **logdetail)
 }
 
 
+static
+bool
+validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
+{
+	char	  **current_secrets;
+	char	   *salt1, *salt2 = NULL;
+	int			i, num;
+	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);
+	if (num == 0)
+	{
+		*salt = NULL; /* No existing passwords, allow one to be generated */
+		return true;
+	}
+	for (i = 0; i < num; 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;
+				uint8		stored_key[SCRAM_KEY_LEN];
+				uint8		server_key[SCRAM_KEY_LEN];
+
+				parse_scram_secret(current_secrets[i], &iterations, &salt1,
+									stored_key, server_key);
+
+				if (salt2 != NULL) {
+					if (strcmp(salt1, salt2)) {
+						*logdetail = psprintf(_("inconsistent salts, clearing password"));
+						*salt = NULL;
+						return false;
+					}
+				}
+				else
+					salt2 = salt1;
+		}
+
+	}
+	for (i = 0; i < num; i++)
+		pfree(current_secrets[i]);
+	pfree(current_secrets);
+	if (salt2)
+		*salt = pstrdup(salt2);
+	return true;
+}
+
 /*
  * CREATE ROLE
  */
@@ -117,6 +182,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	ListCell   *item;
 	ListCell   *option;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* name of the password for managing multiple passwords */
 	bool		issuper = false;	/* Make the user a superuser? */
 	bool		inherit = true; /* Auto inherit privileges? */
 	bool		createrole = false; /* Can this user create roles? */
@@ -144,6 +210,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dadminmembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -246,6 +314,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -283,6 +358,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		validUntil = strVal(dvalidUntil->arg);
 	if (dbypassRLS)
 		bypassrls = boolVal(dbypassRLS->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/* Check some permissions first */
 	if (issuper)
@@ -424,8 +501,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 	if (password)
 	{
-		char	   *shadow_pass;
-		char	   *logdetail;
+		char	   *shadow_pass, *salt;
+		const char	   *logdetail;
 		Datum		new_password_record[Natts_pg_auth_password];
 		bool		new_password_record_nulls[Natts_pg_auth_password];
 		Relation	pg_auth_password_rel;
@@ -453,12 +530,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		else
 		{
 			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+			validate_and_get_salt(stmt->role, &salt, &logdetail);
+			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
 			MemSet(new_password_record, 0, sizeof(new_password_record));
 			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
-
+			if (passname != NULL)
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(passname));
+			else
+				new_password_record[Anum_pg_auth_password_name - 1] = 
+								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -560,6 +643,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* password name for managing multiple passwords */
 	int			connlimit = -1; /* maximum connections allowed */
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
@@ -575,6 +659,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -652,6 +737,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -669,6 +760,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -805,8 +898,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	/* password */
 	if (password)
 	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
+		char	*salt = NULL;
+		const char 	*logdetail = NULL;
+		char	*shadow_pass;
 
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (password[0] == '\0' ||
@@ -818,13 +912,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else
 		{
-			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
-			new_password_record[Anum_pg_auth_password_password - 1] =
-				CStringGetTextDatum(shadow_pass);
+			if (!validate_and_get_salt(rolename, &salt, &logdetail))
+				new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+			else
+			{
+				/* Encrypt the password to the requested format. */
+				shadow_pass = encrypt_password(Password_encryption, salt,
+											password);
+				new_password_record[Anum_pg_auth_password_password - 1] =
+					CStringGetTextDatum(shadow_pass);
+
+				new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+				if (passname)
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(passname));
+				else
+					new_password_record[Anum_pg_auth_password_name - 1] =
+					DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+			}
 		}
-		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
@@ -859,7 +966,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
-		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+		if (dpassName)
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+		else
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname));
 
 		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
 		{
@@ -1157,14 +1267,23 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
 		/*
-		 * Drop password
+		 * Drop password(s)
 		 */
-		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
-		if (HeapTupleIsValid(tuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
-			ReleaseSysCache(tuple);
+		ScanKeyInit(&scankey,
+					Anum_pg_auth_password_roleid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(roleid));
+
+		sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								   true, NULL, 1, &scankey);
+
+		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+		{
+			CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
 		}
 
+		systable_endscan(sscan);
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1201,6 +1320,9 @@ RenameRole(const char *oldname, const char *newname)
 				newtuple,
 				passtuple;
 	TupleDesc	dsc;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+
 	Relation	rel;
 	Datum		datum;
 	bool		isnull = true;
@@ -1211,6 +1333,7 @@ RenameRole(const char *oldname, const char *newname)
 	Oid			roleid;
 	ObjectAddress address;
 	Form_pg_authid authform;
+	Relation	pg_auth_password_rel;
 
 	rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	dsc = RelationGetDescr(rel);
@@ -1301,28 +1424,38 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_password_roleid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(roleid));
+
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
-	if (HeapTupleIsValid(passtuple))
-		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
-								Anum_pg_auth_password_password, &isnull);
+	sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								true, NULL, 1, &scankey);
 
-	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+	while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
 	{
-		Relation	pg_auth_password_rel;
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+							Anum_pg_auth_password_password, &isnull);
 
-		/* MD5 uses the username as salt, so just clear it on a rename */
-		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+		{
 
-		if (HeapTupleIsValid(passtuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
-			ReleaseSysCache(passtuple);
+			/* MD5 uses the username as salt, so just clear it on a rename */
+
+			if (HeapTupleIsValid(passtuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+				ereport(NOTICE,
+					(errmsg("MD5 password cleared because of role rename")));
+
+			}
 		}
-		table_close(pg_auth_password_rel, NoLock);
-		ereport(NOTICE,
-				(errmsg("MD5 password cleared because of role rename")));
 	}
 
+	systable_endscan(sscan);
+	table_close(pg_auth_password_rel, NoLock);
+
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
 
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 805b3695b78..652c18fa94a 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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,
 			  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);
 
 			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 795f1cba555..95db4aaf958 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,13 @@ typedef enum
 	SCRAM_AUTH_FINISHED
 } scram_state_enum;
 
+typedef struct
+{
+	uint8		StoredKey[SCRAM_KEY_LEN];
+	uint8		ServerKey[SCRAM_KEY_LEN];
+} scram_secret;
+
+
 typedef struct
 {
 	scram_state_enum state;
@@ -141,10 +148,16 @@ typedef struct
 	Port	   *port;
 	bool		channel_binding_in_use;
 
+	/* 
+	 * 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_KEY_LEN];
-	uint8		ServerKey[SCRAM_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;
@@ -220,17 +233,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_auth_password.password.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
  * 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;
@@ -260,46 +276,48 @@ 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->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->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 */
+							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;
 	}
 
 	/*
@@ -310,8 +328,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 */
 	if (!got_secret)
 	{
+		state->secrets = palloc0(sizeof(scram_secret));
+
 		mock_scram_secret(state->port->user_name, &state->iterations,
-						  &state->salt, state->StoredKey, state->ServerKey);
+						  &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey);
 		state->doomed = true;
 	}
 
@@ -459,7 +479,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -476,12 +496,14 @@ 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)
+		pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN);
+	
 	result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
 								SCRAM_DEFAULT_ITERATIONS, password,
 								&errstr);
@@ -1114,47 +1136,57 @@ verify_client_proof(scram_state *state)
 	uint8		ClientSignature[SCRAM_KEY_LEN];
 	uint8		ClientKey[SCRAM_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_KEY_LEN];
-	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
-	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, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 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(PG_SHA256);
+		elog(LOG, "Trying to verify password %d", j);
+
+		if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 0)
+		{
+			elog(LOG, "could not calculate client signature");
+			pg_hmac_free(ctx);
+			continue;
+		}
 
-	pg_hmac_free(ctx);
+		elog(LOG, "success on %d", j);
 
-	/* Extract the ClientKey that the client calculated from the proof */
-	for (i = 0; i < SCRAM_KEY_LEN; 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, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
-		elog(ERROR, "could not hash stored key: %s", errstr);
+		for (i = 0; i < SCRAM_KEY_LEN; i++)
+			ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
-	if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
-		return false;
+		/* Hash it one more time, and compare with StoredKey */
+		if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
+			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	return true;
+		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) {
+			elog(LOG, "Moving forward with Password %d", j);
+			state->chosen_secret = j;
+			return true; 
+		}
+	}
+
+	return false;
 }
 
 /*
@@ -1380,7 +1412,7 @@ build_server_final_message(scram_state *state)
 	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
 
 	/* calculate ServerSignature */
-	if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 ||
+	if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 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 5cfde1eaa13..5569599ab31 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -58,8 +58,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, const char **logdetail);
 
 
 /*----------------------------------------------------------------
@@ -790,8 +789,9 @@ static int
 CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
-	int			result;
-	char	   *shadow_pass;
+	int			result = STATUS_ERROR;
+	int			i, num;
+	char	   **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -799,17 +799,21 @@ 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)
-	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
-	}
-	else
-		result = STATUS_ERROR;
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
+	if (passwords != NULL) {
+		for (i = 0; i < num; 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; passwords[i] != NULL; i++) 
+			pfree(passwords[i]);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		pfree(passwords);
+	}
+	
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -824,29 +828,15 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 static int
 CheckPWChallengeAuth(Port *port, const char **logdetail)
 {
-	int			auth_result;
-	char	   *shadow_pass;
-	PasswordType pwtype;
+	int			auth_result = STATUS_ERROR;
+	int			i, num;
+	char	  **passwords;
 
 	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);
-
-	/*
-	 * 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
-	 * 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)
-		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
 
 	/*
 	 * If 'md5' authentication is allowed, decide whether to perform 'md5' or
@@ -858,23 +848,17 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 	 * 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);
-	else
-		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
-									logdetail);
+	if (passwords != NULL) {
+		if (port->hba->auth_method == uaMD5)
+			auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
+											logdetail);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		for (i = 0; i < num; i++) 
+			pfree(passwords[i]);
 
-	/*
-	 * If get_role_password() returned error, return error, even if the
-	 * authentication succeeded.
-	 */
-	if (!shadow_pass)
-	{
-		Assert(auth_result != STATUS_OK);
-		return STATUS_ERROR;
+		pfree(passwords);
 	}
 
 	if (auth_result == STATUS_OK)
@@ -884,11 +868,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, const char **logdetail)
 {
 	char		md5Salt[4];		/* Password salt */
 	char	   *passwd;
 	int			result;
+	int			i;
 
 	if (Db_user_namespace)
 		ereport(FATAL,
@@ -909,12 +894,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; 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 745e61034c2..09b6bebf40c 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -23,6 +23,7 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
@@ -34,13 +35,15 @@
  * 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)
 {
-	HeapTuple	roleTup, passTup;
+	HeapTuple	roleTup;
 	Datum		datum;
 	bool		isnull;
-	char	   *shadow_pass;
+	char	   **passwords;
+	CatCList   *passlist;
+	int		    i;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,30 +55,34 @@ get_role_password(const char *role, const char **logdetail)
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
-	
-	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+	ReleaseSysCache(roleTup);
+	/* Find any existing password that is not the one being updated to get the salt */
+	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+	*num = passlist->n_members;
 
-	if (!HeapTupleIsValid(passTup))
+	if (passlist->n_members == 0)
 	{
+		*num = 0;
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
+		ReleaseCatCacheList(passlist);
 		return NULL;			/* user has no password */
 	}
-	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
-							Anum_pg_auth_password_password, &isnull);
 
-	ReleaseSysCache(roleTup);
-	if (isnull) /* this should not happen any more but just in case */
+	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+
+	for (i = 0; i < passlist->n_members; i++)
 	{
-		ReleaseSysCache(passTup);
-		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
-							  role);
-		return NULL;			/* user has no password */
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_password, &isnull);
+		passwords[i] = TextDatumGetCString(datum);
 	}
-	shadow_pass = TextDatumGetCString(datum);
-	ReleaseSysCache(passTup);
 
-	return shadow_pass;
+	ReleaseCatCacheList(passlist);
+
+	return passwords;
 }
 
 /*
@@ -107,7 +114,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);
@@ -128,13 +135,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);
+				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'");
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9941d9cb4f..c3cb6c76aa5 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -826,8 +826,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
-	PLACING PLAN PLANS POLICY
+
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD 
+	PATH PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -1256,6 +1257,15 @@ AlterOptRoleElem:
 							 errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
 							 parser_errposition(@1)));
 				}
+			| PASSNAME Sconst
+				{
+					$$ = makeDefElem("passname",
+									 (Node *)makeString($2), @1);
+				}
+			| VALID FOR Sconst
+				{
+					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+				}
 			| INHERIT
 				{
 					$$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
@@ -17082,6 +17092,7 @@ unreserved_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PATH
 			| PLAN
@@ -17700,6 +17711,7 @@ bare_label_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PATH
 			| PLACING
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 356e9cde6fb..c421ac0b19f 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHPASSWORD:
+		case AUTHPASSWORDNAME:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index de2b3ec1ebf..9879120bf76 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -256,12 +256,12 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
-	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+	{AuthPasswordRelationId,			/* AUTHPASSWORDNAME */
 		AuthPasswordRoleOidIndexId,
-		1,
+		2,
 		{
 			Anum_pg_auth_password_roleid,
-			0,
+			Anum_pg_auth_password_name,
 			0,
 			0
 		},
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index beaa2d40b90..4181caad30b 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -24,11 +24,13 @@
  *		typedef struct FormData_pg_auth_password
  * ----------------
  */
-CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
 {
 	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+    NameData    name;            /* name of password for multiple password support */
+
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
-    text            password;        /* password */
+    text            password BKI_FORCE_NOT_NULL;        /* password */
 #endif
 } FormData_pg_auth_password;
 
@@ -44,7 +46,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176);
 
 typedef FormData_pg_auth_password *Form_pg_auth_password;
 
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
-
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops));
 
 #endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 3e1aae57854..1c14f60a05b 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
-extern bool is_role_valid(const char *rolename, char **logdetail);
+extern bool is_role_valid(const char *rolename, const char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index b8ff8ccb417..84c9c4b4f48 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 39ccf8f0e31..27b4e570333 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, const char **logdetail);
 
 #endif							/* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index c51e848c24d..70b01b511f0 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,7 @@
 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, char **salt,
 							   uint8 *stored_key, uint8 *server_key);
 extern bool scram_verify_plain_password(const char *username,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 8a2ab405a28..bf84593a61c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -334,6 +334,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c7bc648f46a..a543c181889 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,7 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
-	AUTHPASSWORD,
+	AUTHPASSWORDNAME,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 4ffc41a5455..c6a84f86a84 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 -- should not contain the original salt.
 SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
-- 
2.35.1

#5Jacob Champion
jchampion@timescale.com
In reply to: Joshua Brindle (#4)
Re: [PoC/RFC] Multiple passwords, interval expirations

On 4/8/22 10:04, Joshua Brindle wrote:

It's unclear if I will be able to continue working on this featureset,
this email address will be inactive after today.

I'm assuming the answer to this was "no". Is there any interest out
there to pick this up for the July CF?

--Jacob

#6Stephen Frost
sfrost@snowman.net
In reply to: Jacob Champion (#5)
Re: [PoC/RFC] Multiple passwords, interval expirations

Greetings,

On Wed, Jun 29, 2022 at 17:22 Jacob Champion <jchampion@timescale.com>
wrote:

On 4/8/22 10:04, Joshua Brindle wrote:

It's unclear if I will be able to continue working on this featureset,
this email address will be inactive after today.

I'm assuming the answer to this was "no". Is there any interest out
there to pick this up for the July CF?

Short answer to that is yes, I’m interested in continuing this (though
certainly would welcome it if there are others who are also interested, and
may be able to bring someone else to help work on it too but that might be
more August / September time frame).

Thanks,

Stephen

Show quoted text
#7Gurjeet Singh
gurjeet@singh.im
In reply to: Stephen Frost (#6)
Re: [PoC/RFC] Multiple passwords, interval expirations

I am planning on picking it up next week; right now picking up steam,
and reviewing a different, smaller patch.

At his behest, I had a conversation with Joshua (OP), and have his
support to pick up and continue working on this patch. I have a some
ideas of my own, on what this patch should do, but since I haven't
fully reviewed the (bulky) patch, I'll reserve my proposals until I
wrap my head around it.

Please expect some activity on this patch towards the end of next week.

BCC: Joshua's new work email.

Best regards,
Gurjeet
http://Gurje.et

Show quoted text

On Wed, Jun 29, 2022 at 2:27 PM Stephen Frost <sfrost@snowman.net> wrote:

Greetings,

On Wed, Jun 29, 2022 at 17:22 Jacob Champion <jchampion@timescale.com> wrote:

On 4/8/22 10:04, Joshua Brindle wrote:

It's unclear if I will be able to continue working on this featureset,
this email address will be inactive after today.

I'm assuming the answer to this was "no". Is there any interest out
there to pick this up for the July CF?

Short answer to that is yes, I’m interested in continuing this (though certainly would welcome it if there are others who are also interested, and may be able to bring someone else to help work on it too but that might be more August / September time frame).

Thanks,

Stephen

#8Stephen Frost
sfrost@snowman.net
In reply to: Gurjeet Singh (#7)
1 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

Greetings,

* Gurjeet Singh (gurjeet@singh.im) wrote:

I am planning on picking it up next week; right now picking up steam,
and reviewing a different, smaller patch.

Great! Glad that others are interested in this.

At his behest, I had a conversation with Joshua (OP), and have his
support to pick up and continue working on this patch. I have a some
ideas of my own, on what this patch should do, but since I haven't
fully reviewed the (bulky) patch, I'll reserve my proposals until I
wrap my head around it.

I'd be curious as to your thought as to what the patch should be doing.
Joshua and I had discussed it at some length as he was working on it.

Please expect some activity on this patch towards the end of next week.

I've gone ahead and updated it, cleaned up a couple things, and make it
so that check-world actually passes with it. Attached is an updated
version and I'll add it to the July commitfest.

Thanks!

Stephen

Attachments:

multipass-v3.patchtext/x-diff; charset=us-asciiDownload
From 1203b8a92e4e6c51c7e5aa244d8ee9bbd2f74e66 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Thu, 30 Jun 2022 18:15:52 -0400
Subject: [PATCH 1/3] Move rolpassword out of pg_authid into a new table

In preparation for more flexibility in password
management the rolpassword column needs to be
moved into a new table.

Author: Joshua Brindle
---
 src/backend/catalog/Makefile                 |   2 +-
 src/backend/catalog/catalog.c                |   8 +-
 src/backend/catalog/system_views.sql         |   5 +-
 src/backend/commands/user.c                  | 226 ++++++++++++----
 src/backend/libpq/auth-sasl.c                |   2 +-
 src/backend/libpq/auth-scram.c               |   4 +-
 src/backend/libpq/auth.c                     |  16 ++
 src/backend/libpq/crypt.c                    |  40 ++-
 src/backend/utils/cache/catcache.c           |   1 +
 src/backend/utils/cache/relcache.c           |  13 +-
 src/backend/utils/cache/syscache.c           |  12 +
 src/bin/initdb/initdb.c                      |   2 +-
 src/bin/pg_dump/pg_dumpall.c                 |  13 +-
 src/common/scram-common.c                    |   2 +-
 src/include/catalog/pg_auth_password.h       |  50 ++++
 src/include/catalog/pg_authid.dat            |  26 +-
 src/include/catalog/pg_authid.h              |   7 +-
 src/include/commands/user.h                  |   1 +
 src/include/libpq/crypt.h                    |   2 +-
 src/include/utils/syscache.h                 |   1 +
 src/test/regress/expected/create_index.out   |  14 +-
 src/test/regress/expected/oidjoins.out       |   1 +
 src/test/regress/expected/password.out       |  44 ++--
 src/test/regress/expected/roleattributes.out | 256 +++++++++----------
 src/test/regress/expected/rules.out          |   5 +-
 src/test/regress/expected/tablespace.out     |  12 +-
 src/test/regress/sql/create_index.sql        |  10 +-
 src/test/regress/sql/password.sql            |  32 ++-
 src/test/regress/sql/roleattributes.sql      |  64 ++---
 src/test/regress/sql/tablespace.sql          |   8 +-
 30 files changed, 563 insertions(+), 316 deletions(-)
 create mode 100644 src/include/catalog/pg_auth_password.h

diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..450cfd9a58 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -63,7 +63,7 @@ CATALOG_HEADERS := \
 	pg_rewrite.h pg_trigger.h pg_event_trigger.h pg_description.h \
 	pg_cast.h pg_enum.h pg_namespace.h pg_conversion.h pg_depend.h \
 	pg_database.h pg_db_role_setting.h pg_tablespace.h \
-	pg_authid.h pg_auth_members.h pg_shdepend.h pg_shdescription.h \
+	pg_authid.h pg_auth_members.h pg_auth_password.h pg_shdepend.h pg_shdescription.h \
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index e784538aae..6a32e44c39 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -28,6 +28,7 @@
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
@@ -246,6 +247,7 @@ IsSharedRelation(Oid relationId)
 {
 	/* These are the shared catalogs (look for BKI_SHARED_RELATION) */
 	if (relationId == AuthIdRelationId ||
+		relationId == AuthPasswordRelationId ||
 		relationId == AuthMemRelationId ||
 		relationId == DatabaseRelationId ||
 		relationId == DbRoleSettingRelationId ||
@@ -260,6 +262,8 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
+		relationId == AuthPasswordRoleOidIndexId ||
+		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
 		relationId == DatabaseNameIndexId ||
@@ -279,8 +283,8 @@ IsSharedRelation(Oid relationId)
 		relationId == TablespaceOidIndexId)
 		return true;
 	/* These are their toast tables and toast indexes */
-	if (relationId == PgAuthidToastTable ||
-		relationId == PgAuthidToastIndex ||
+	if (relationId == PgAuthPasswordToastTable ||
+		relationId == PgAuthPasswordToastIndex ||
 		relationId == PgDatabaseToastTable ||
 		relationId == PgDatabaseToastIndex ||
 		relationId == PgDbRoleSettingToastTable ||
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index fedaed533b..6989594022 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -40,11 +40,14 @@ CREATE VIEW pg_shadow AS
         rolsuper AS usesuper,
         rolreplication AS userepl,
         rolbypassrls AS usebypassrls,
-        rolpassword AS passwd,
+        p.password AS passwd,
         rolvaliduntil AS valuntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
+    LEFT JOIN pg_auth_password p
+    ON p.roleid = pg_authid.oid
+
     WHERE rolcanlogin;
 
 REVOKE ALL ON pg_shadow FROM public;
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 984305ba31..b224b4a2e9 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -23,6 +23,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "commands/comment.h"
@@ -64,6 +65,42 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+/*
+ * Is the role able to log in
+*/
+bool
+is_role_valid(const char *rolename, char **logdetail)
+{
+	HeapTuple	roleTup;
+	Datum		datum;
+	bool		isnull;
+	TimestampTz vuntil = 0;
+
+	/* Get role info from pg_authid */
+	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(rolename));
+	if (!HeapTupleIsValid(roleTup))
+	{
+		*logdetail = psprintf(_("Role \"%s\" does not exist."),
+							  rolename);
+		return false;			/* no such user */
+	}
+
+	datum = SysCacheGetAttr(AUTHNAME, roleTup,
+							Anum_pg_authid_rolvaliduntil, &isnull);
+	ReleaseSysCache(roleTup);
+
+	if (!isnull)
+		vuntil = DatumGetTimestampTz(datum);
+
+	if (!isnull && vuntil < GetCurrentTimestamp())
+	{
+		*logdetail = psprintf(_("User \"%s\" has an expired password."), rolename);
+		return false;
+	}
+
+	return true;
+}
+
 
 /*
  * CREATE ROLE
@@ -351,43 +388,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
 	new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
 	new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
-
-	if (password)
-	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
-
-		/*
-		 * Don't allow an empty password. Libpq treats an empty password the
-		 * same as no password at all, and won't even try to authenticate. But
-		 * other clients might, so allowing it would be confusing. By clearing
-		 * the password when an empty string is specified, the account is
-		 * consistently locked for all clients.
-		 *
-		 * Note that this only covers passwords stored in the database itself.
-		 * There are also checks in the authentication code, to forbid an
-		 * empty password from being used with authentication methods that
-		 * fetch the password from an external system, like LDAP or PAM.
-		 */
-		if (password[0] == '\0' ||
-			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
-		{
-			ereport(NOTICE,
-					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-		}
-		else
-		{
-			/* 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);
-		}
-	}
-	else
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
@@ -422,6 +422,60 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	CatalogTupleInsert(pg_authid_rel, tuple);
 
+	if (password)
+	{
+		char	   *shadow_pass;
+		char	   *logdetail;
+		Datum		new_password_record[Natts_pg_auth_password];
+		bool		new_password_record_nulls[Natts_pg_auth_password];
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple	new_tuple;
+
+		/*
+		 * Don't allow an empty password. Libpq treats an empty password the
+		 * same as no password at all, and won't even try to authenticate. But
+		 * other clients might, so allowing it would be confusing. By clearing
+		 * the password when an empty string is specified, the account is
+		 * consistently locked for all clients.
+		 *
+		 * Note that this only covers passwords stored in the database itself.
+		 * There are also checks in the authentication code, to forbid an
+		 * empty password from being used with authentication methods that
+		 * fetch the password from an external system, like LDAP or PAM.
+		 */
+		if (password[0] == '\0' ||
+			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+		}
+		else
+		{
+			/* Encrypt the password to the requested format. */
+			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+										   password);
+
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			/* open password table and insert it. */
+			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+
+			new_password_record[Anum_pg_auth_password_password - 1] =
+				CStringGetTextDatum(shadow_pass);
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+			table_close(pg_auth_password_rel, NoLock);
+
+		}
+	}
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -495,6 +549,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	Datum		new_record[Natts_pg_authid];
 	bool		new_record_nulls[Natts_pg_authid];
 	bool		new_record_repl[Natts_pg_authid];
+	Datum		new_password_record[Natts_pg_auth_password];
+	bool		new_password_record_nulls[Natts_pg_auth_password];
+	bool		new_password_record_repl[Natts_pg_auth_password];
 	Relation	pg_authid_rel;
 	TupleDesc	pg_authid_dsc;
 	HeapTuple	tuple,
@@ -624,6 +681,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	rolename = pstrdup(NameStr(authform->rolname));
 	roleid = authform->oid;
 
+
 	/*
 	 * To mess with a superuser or replication role in any way you gotta be
 	 * superuser.  We also insist on superuser to change the BYPASSRLS
@@ -694,6 +752,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_record, 0, sizeof(new_record));
 	MemSet(new_record_nulls, false, sizeof(new_record_nulls));
 	MemSet(new_record_repl, false, sizeof(new_record_repl));
+	MemSet(new_password_record, 0, sizeof(new_password_record));
+	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
+
 
 	/*
 	 * issuper/createrole/etc
@@ -752,24 +814,24 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		{
 			ereport(NOTICE,
 					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+			new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 		}
 		else
 		{
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, rolename,
 										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
+			new_password_record[Anum_pg_auth_password_password - 1] =
 				CStringGetTextDatum(shadow_pass);
 		}
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
 	if (dpassword && dpassword->arg == NULL)
 	{
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+		new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* valid until */
@@ -786,12 +848,48 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
 								  new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
-
-	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
-
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	{
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple   password_tuple;
+
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+
+		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
+		{
+			if (HeapTupleIsValid(password_tuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &password_tuple->t_self);
+				ReleaseSysCache(password_tuple);
+			}
+		}
+		else if (HeapTupleIsValid(password_tuple)) /* update existing password */
+		{
+			new_tuple = heap_modify_tuple(password_tuple, pg_auth_password_dsc, new_password_record,
+										new_password_record_nulls, new_password_record_repl);
+			CatalogTupleUpdate(pg_auth_password_rel, &password_tuple->t_self, new_tuple);
+			ReleaseSysCache(password_tuple);
+			heap_freetuple(new_tuple);
+		}
+		else /* insert new password */
+		{
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+		}
+
+		table_close(pg_auth_password_rel, NoLock);
+	}
+	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -901,7 +999,6 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 	return roleid;
 }
 
-
 /*
  * DROP ROLE
  */
@@ -909,7 +1006,8 @@ void
 DropRole(DropRoleStmt *stmt)
 {
 	Relation	pg_authid_rel,
-				pg_auth_members_rel;
+				pg_auth_members_rel,
+				pg_auth_password_rel;
 	ListCell   *item;
 
 	if (!have_createrole_privilege())
@@ -923,6 +1021,8 @@ DropRole(DropRoleStmt *stmt)
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+
 
 	foreach(item, stmt->roles)
 	{
@@ -1056,6 +1156,15 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedComments(roleid, AuthIdRelationId);
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
+		/*
+		 * Drop password
+		 */
+		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
+		if (HeapTupleIsValid(tuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
+			ReleaseSysCache(tuple);
+		}
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1077,7 +1186,9 @@ DropRole(DropRoleStmt *stmt)
 	 * Now we can clean up; but keep locks until commit.
 	 */
 	table_close(pg_auth_members_rel, NoLock);
+	table_close(pg_auth_password_rel, NoLock);
 	table_close(pg_authid_rel, NoLock);
+
 }
 
 /*
@@ -1087,11 +1198,12 @@ ObjectAddress
 RenameRole(const char *oldname, const char *newname)
 {
 	HeapTuple	oldtuple,
-				newtuple;
+				newtuple,
+				passtuple;
 	TupleDesc	dsc;
 	Relation	rel;
 	Datum		datum;
-	bool		isnull;
+	bool		isnull = true;
 	Datum		repl_val[Natts_pg_authid];
 	bool		repl_null[Natts_pg_authid];
 	bool		repl_repl[Natts_pg_authid];
@@ -1189,14 +1301,24 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
+	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+
+	if (HeapTupleIsValid(passtuple))
+		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
+								Anum_pg_auth_password_password, &isnull);
 
 	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
 	{
+		Relation	pg_auth_password_rel;
+
 		/* MD5 uses the username as salt, so just clear it on a rename */
-		repl_repl[Anum_pg_authid_rolpassword - 1] = true;
-		repl_null[Anum_pg_authid_rolpassword - 1] = true;
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
+		if (HeapTupleIsValid(passtuple)) {
+			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+			ReleaseSysCache(passtuple);
+		}
+		table_close(pg_auth_password_rel, NoLock);
 		ereport(NOTICE,
 				(errmsg("MD5 password cleared because of role rename")));
 	}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index a1d7dbb6d5..805b3695b7 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -33,7 +33,7 @@
  * implementation.
  *
  * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
+ * authenticated, from pg_auth_password.password.  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.
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ee7f52218a..795f1cba55 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -220,7 +220,7 @@ 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.
+ * 'shadow_pass' is the role's stored secret, from pg_auth_password.password.
  * 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
@@ -454,7 +454,7 @@ 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_auth_password.password.
  *
  * The result is palloc'd, so caller is responsible for freeing it.
  */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index efc53f3135..269cf286ad 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -592,10 +592,26 @@ ClientAuthentication(Port *port)
 
 		case uaMD5:
 		case uaSCRAM:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+			 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPWChallengeAuth(port, &logdetail);
 			break;
 
 		case uaPassword:
+			/*
+			 * check to be sure we are not past rolvaliduntil
+			 */
+			if (!is_role_valid(port->user_name, &logdetail)) {
+				status = STATUS_ERROR;
+				break;
+			}
+
 			status = CheckPasswordAuth(port, &logdetail);
 			break;
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 1ff8b0507d..745e61034c 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_auth_password.password.
  *
  * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -16,6 +16,7 @@
 #include <unistd.h>
 
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "common/md5.h"
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
@@ -36,8 +37,7 @@
 char *
 get_role_password(const char *role, const char **logdetail)
 {
-	TimestampTz vuntil = 0;
-	HeapTuple	roleTup;
+	HeapTuple	roleTup, passTup;
 	Datum		datum;
 	bool		isnull;
 	char	   *shadow_pass;
@@ -51,33 +51,29 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;			/* no such user */
 	}
 
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
+	
+	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+
+	if (!HeapTupleIsValid(passTup))
 	{
-		ReleaseSysCache(roleTup);
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
-
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
-	if (!isnull)
-		vuntil = DatumGetTimestampTz(datum);
+	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
+							Anum_pg_auth_password_password, &isnull);
 
 	ReleaseSysCache(roleTup);
-
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (isnull) /* this should not happen any more but just in case */
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
+		ReleaseSysCache(passTup);
+		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
-		return NULL;
+		return NULL;			/* user has no password */
 	}
+	shadow_pass = TextDatumGetCString(datum);
+	ReleaseSysCache(passTup);
 
 	return shadow_pass;
 }
@@ -156,7 +152,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_auth_password.password.
  * '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.
  *
@@ -211,7 +207,7 @@ 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_auth_password.password.
  * '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/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 6ae7c1f50b..7c8f864a80 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,6 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
+		case AUTHPASSWORD:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index f502df91dc..b2ca82d1ab 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -50,6 +50,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_namespace.h"
@@ -113,6 +114,7 @@ static const FormData_pg_attribute Desc_pg_proc[Natts_pg_proc] = {Schema_pg_proc
 static const FormData_pg_attribute Desc_pg_type[Natts_pg_type] = {Schema_pg_type};
 static const FormData_pg_attribute Desc_pg_database[Natts_pg_database] = {Schema_pg_database};
 static const FormData_pg_attribute Desc_pg_authid[Natts_pg_authid] = {Schema_pg_authid};
+static const FormData_pg_attribute Desc_pg_auth_password[Natts_pg_auth_password] = {Schema_pg_auth_password};
 static const FormData_pg_attribute Desc_pg_auth_members[Natts_pg_auth_members] = {Schema_pg_auth_members};
 static const FormData_pg_attribute Desc_pg_index[Natts_pg_index] = {Schema_pg_index};
 static const FormData_pg_attribute Desc_pg_shseclabel[Natts_pg_shseclabel] = {Schema_pg_shseclabel};
@@ -3485,6 +3487,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case DatabaseRelationId:
 		case AuthIdRelationId:
+		case AuthPasswordRelationId:
 		case AuthMemRelationId:
 		case RelationRelationId:
 		case AttributeRelationId:
@@ -3905,7 +3908,7 @@ RelationCacheInitialize(void)
  *		RelationCacheInitializePhase2
  *
  *		This is called to prepare for access to shared catalogs during startup.
- *		We must at least set up nailed reldescs for pg_database, pg_authid,
+ *		We must at least set up nailed reldescs for pg_database, pg_authid, pg_auth_password,
  *		pg_auth_members, and pg_shseclabel. Ideally we'd like to have reldescs
  *		for their indexes, too.  We attempt to load this information from the
  *		shared relcache init file.  If that's missing or broken, just make
@@ -3950,8 +3953,10 @@ RelationCacheInitializePhase2(void)
 				  Natts_pg_shseclabel, Desc_pg_shseclabel);
 		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
 				  Natts_pg_subscription, Desc_pg_subscription);
+		formrdesc("pg_auth_password", AuthPasswordRelation_Rowtype_Id, true,
+				  Natts_pg_auth_password, Desc_pg_auth_password);
 
-#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_RELS	6	/* fix if you change list above */
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -4090,8 +4095,10 @@ RelationCacheInitializePhase3(void)
 							AuthMemRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
+		load_critical_index(AuthPasswordRoleOidIndexId,
+							AuthPasswordRelationId);
 
-#define NUM_CRITICAL_SHARED_INDEXES 6	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_INDEXES 7	/* fix if you change list above */
 
 		criticalSharedRelcachesBuilt = true;
 	}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..de2b3ec1eb 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -255,6 +256,17 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+		AuthPasswordRoleOidIndexId,
+		1,
+		{
+			Anum_pg_auth_password_roleid,
+			0,
+			0,
+			0
+		},
+		8
+	},
 	{
 		CastRelationId,			/* CASTSOURCETARGET */
 		CastSourceTargetIndexId,
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index ed6de7ca94..7a39980e05 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1402,7 +1402,7 @@ setup_auth(FILE *cmdfd)
 		 * The authid table shouldn't be readable except through views, to
 		 * ensure passwords are not publicly visible.
 		 */
-		"REVOKE ALL ON pg_authid FROM public;\n\n",
+		"REVOKE ALL ON pg_auth_password FROM public;\n\n",
 		NULL
 	};
 
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index ae41a652d7..64f27aa3f2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -737,7 +737,18 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* note: rolconfig is dumped later */
-	if (server_version >= 90600)
+	if (server_version >= 150000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, p.password as rolpassword, "
+						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s LEFT JOIN pg_auth_password p ON %s.oid = p.roleid "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog, role_catalog, role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index 1268625929..d9d0cba737 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -181,7 +181,7 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result,
 
 
 /*
- * Construct a SCRAM secret, for storing in pg_authid.rolpassword.
+ * Construct a SCRAM secret, for storing in pg_auth_password.password.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
new file mode 100644
index 0000000000..beaa2d40b9
--- /dev/null
+++ b/src/include/catalog/pg_auth_password.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_auth_password.h
+ *	  definition of the "authorization identifier" system catalog (pg_auth_password)
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_auth_password.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_AUTH_PASSWORD_H
+#define PG_AUTH_PASSWORD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_auth_password_d.h"
+
+/* ----------------
+ *		pg_auth_password definition.  cpp turns this into
+ *		typedef struct FormData_pg_auth_password
+ * ----------------
+ */
+CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+{
+	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+#ifdef CATALOG_VARLEN                /* variable-length fields start here */
+    text            password;        /* password */
+#endif
+} FormData_pg_auth_password;
+
+/* ----------------
+ *		Form_pg_auth_password corresponds to a pointer to a tuple with
+ *		the format of pg_auth_password relation.
+ * ----------------
+ */
+
+DECLARE_TOAST(pg_auth_password, 4175, 4176);
+#define PgAuthPasswordToastTable 4175
+#define PgAuthPasswordToastIndex 4176
+
+typedef FormData_pg_auth_password *Form_pg_auth_password;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
+
+
+#endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6c28119fa1..da36375a58 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,66 +23,66 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_null_' },
 { oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINTER',
   rolname => 'pg_checkpointer', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 3512601c80..0cb5675a33 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -42,9 +42,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
-#ifdef CATALOG_VARLEN			/* variable-length fields start here */
-	text		rolpassword;	/* password, if any */
-	timestamptz rolvaliduntil;	/* password expiration time, if any */
+#ifdef CATALOG_VARLEN
+	timestamptz rolvaliduntil BKI_FORCE_NULL;	/* role expiration time, if any */
 #endif
 } FormData_pg_authid;
 
@@ -55,8 +54,6 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
  */
 typedef FormData_pg_authid *Form_pg_authid;
 
-DECLARE_TOAST_WITH_MACRO(pg_authid, 4175, 4176, PgAuthidToastTable, PgAuthidToastIndex);
-
 DECLARE_UNIQUE_INDEX(pg_authid_rolname_index, 2676, AuthIdRolnameIndexId, on pg_authid using btree(rolname name_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_authid_oid_index, 2677, AuthIdOidIndexId, on pg_authid using btree(oid oid_ops));
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index d3dd8303d2..3e1aae5785 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,6 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
+extern bool is_role_valid(const char *rolename, char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index 3238cf66d3..b8ff8ccb41 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,7 +21,7 @@
  * 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
+ * in pg_auth_password.password. They are also the allowed values for the
  * password_encryption GUC.
  */
 typedef enum PasswordType
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 4463ea66be..c7bc648f46 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,6 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
+	AUTHPASSWORD,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index d55aec3a1d..9875b277f2 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2515,10 +2515,10 @@ REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
 ERROR:  cannot reindex system catalogs concurrently
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 ERROR:  cannot reindex system catalogs concurrently
@@ -2817,10 +2817,10 @@ ERROR:  must be owner of schema schema_to_reindex
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
-REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+REINDEX TABLE pg_toast.pg_toast_1262;
+ERROR:  must be owner of table pg_toast_1262
+REINDEX INDEX pg_toast.pg_toast_1262_index;
+ERROR:  must be owner of index pg_toast_1262_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..b69a2a72a7 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_password {roleid} => pg_authid {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 7c84c9da33..4ffc41a545 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -24,10 +24,12 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
@@ -40,12 +42,14 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
-       rolname       | rolpassword 
----------------------+-------------
+    ORDER BY rolname, password;
+       rolname       | password 
+---------------------+----------
  regress_passwd2_new | 
 (1 row)
 
@@ -72,10 +76,12 @@ CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
@@ -95,9 +101,11 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
- rolpassword 
--------------
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p 
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
+ password 
+----------
  
 (1 row)
 
@@ -110,8 +118,10 @@ CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
          rolname         | is_rolpassword_rehashed 
@@ -134,11 +144,13 @@ DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p 
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
- rolname | rolpassword 
----------+-------------
+    ORDER BY rolname, password;
+ rolname | password 
+---------+----------
 (0 rows)
 
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173..18a9dacadd 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -1,233 +1,233 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
-           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
-          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
-           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 -- clean up roles
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index fc3cde3226..231fec7fda 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1725,11 +1725,12 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolsuper AS usesuper,
     pg_authid.rolreplication AS userepl,
     pg_authid.rolbypassrls AS usebypassrls,
-    pg_authid.rolpassword AS passwd,
+    p.password AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
     s.setconfig AS useconfig
-   FROM (pg_authid
+   FROM ((pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
+     LEFT JOIN pg_auth_password p ON ((p.roleid = pg_authid.oid)))
   WHERE pg_authid.rolcanlogin;
 pg_shmem_allocations| SELECT pg_get_shmem_allocations.name,
     pg_get_shmem_allocations.off,
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index c52cf1cfcf..2ca2cd181f 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -53,13 +53,13 @@ ERROR:  cannot move system relation "pg_authid_rolname_index"
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 ERROR:  cannot reindex system catalogs concurrently
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 ERROR:  cannot reindex system catalogs concurrently
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index d8fded3d93..8d649f2355 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -1068,9 +1068,9 @@ REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 COMMIT;
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 -- Warns about catalog relations
 REINDEX SCHEMA CONCURRENTLY pg_catalog;
@@ -1243,8 +1243,8 @@ REINDEX SCHEMA schema_to_reindex;
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-REINDEX INDEX pg_toast.pg_toast_1260_index;
+REINDEX TABLE pg_toast.pg_toast_1262;
+REINDEX INDEX pg_toast.pg_toast_1262_index;
 
 -- Clean up
 RESET ROLE;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 98f49916e5..f41d308fbb 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -23,18 +23,22 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- Rename a role
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 
 -- Change passwords with ALTER USER. With plaintext or already-encrypted
@@ -63,16 +67,20 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
 ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
 
 -- Test with invalid stored and server keys.
 --
@@ -84,8 +92,10 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
 
@@ -103,7 +113,9 @@ DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d730..09505c6a3b 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -1,83 +1,83 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
 
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 
 -- clean up roles
 DROP ROLE regress_test_def_superuser;
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index 21db433f2a..d92135173f 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -42,10 +42,10 @@ REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am;
 REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid;
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
 REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid;
-- 
2.34.1


From bfb15c69fa4367e92cabeef1a97eb6fb394b788f Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Thu, 30 Jun 2022 18:16:52 -0400
Subject: [PATCH 2/3] multiple passwords work with scram and md5

also renaming roles, dropping roles, switching between
password_encryption

Author: Joshua Brindle
---
 src/backend/commands/user.c            | 198 ++++++++++++++++++++-----
 src/backend/libpq/auth-sasl.c          |   4 +-
 src/backend/libpq/auth-scram.c         | 181 ++++++++++++----------
 src/backend/libpq/auth.c               | 103 ++++++-------
 src/backend/libpq/crypt.c              |  51 ++++---
 src/backend/parser/gram.y              |  15 +-
 src/backend/utils/cache/catcache.c     |   2 +-
 src/backend/utils/cache/syscache.c     |   6 +-
 src/include/catalog/pg_auth_password.h |  10 +-
 src/include/commands/user.h            |   2 +-
 src/include/libpq/crypt.h              |   2 +-
 src/include/libpq/sasl.h               |   4 +-
 src/include/libpq/scram.h              |   2 +-
 src/include/parser/kwlist.h            |   1 +
 src/include/utils/syscache.h           |   2 +-
 src/test/regress/expected/password.out |  12 +-
 16 files changed, 385 insertions(+), 210 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index b224b4a2e9..94747d7c3c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -31,11 +31,14 @@
 #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"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/fmgroids.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -47,6 +50,9 @@ Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 /* GUC parameter */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
 
+/* default password name */
+const char* default_passname = "__def__";
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 check_password_hook_type check_password_hook = NULL;
 
@@ -69,7 +75,7 @@ have_createrole_privilege(void)
  * Is the role able to log in
 */
 bool
-is_role_valid(const char *rolename, char **logdetail)
+is_role_valid(const char *rolename, const char **logdetail)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
@@ -102,6 +108,64 @@ is_role_valid(const char *rolename, char **logdetail)
 }
 
 
+static
+bool
+validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
+{
+	char	  **current_secrets;
+	char	   *salt1, *salt2 = NULL;
+	int			i, num;
+	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);
+	if (num == 0)
+	{
+		*salt = NULL; /* No existing passwords, allow one to be generated */
+		return true;
+	}
+	for (i = 0; i < num; 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;
+				uint8		stored_key[SCRAM_KEY_LEN];
+				uint8		server_key[SCRAM_KEY_LEN];
+
+				parse_scram_secret(current_secrets[i], &iterations, &salt1,
+									stored_key, server_key);
+
+				if (salt2 != NULL) {
+					if (strcmp(salt1, salt2)) {
+						*logdetail = psprintf(_("inconsistent salts, clearing password"));
+						*salt = NULL;
+						return false;
+					}
+				}
+				else
+					salt2 = salt1;
+		}
+
+	}
+	for (i = 0; i < num; i++)
+		pfree(current_secrets[i]);
+	pfree(current_secrets);
+	if (salt2)
+		*salt = pstrdup(salt2);
+	return true;
+}
+
 /*
  * CREATE ROLE
  */
@@ -117,6 +181,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	ListCell   *item;
 	ListCell   *option;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* name of the password for managing multiple passwords */
 	bool		issuper = false;	/* Make the user a superuser? */
 	bool		inherit = true; /* Auto inherit privileges? */
 	bool		createrole = false; /* Can this user create roles? */
@@ -144,6 +209,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dadminmembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -246,6 +313,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -283,6 +357,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		validUntil = strVal(dvalidUntil->arg);
 	if (dbypassRLS)
 		bypassrls = boolVal(dbypassRLS->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/* Check some permissions first */
 	if (issuper)
@@ -424,8 +500,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 	if (password)
 	{
-		char	   *shadow_pass;
-		char	   *logdetail;
+		char	   *shadow_pass, *salt;
+		const char	   *logdetail;
 		Datum		new_password_record[Natts_pg_auth_password];
 		bool		new_password_record_nulls[Natts_pg_auth_password];
 		Relation	pg_auth_password_rel;
@@ -453,12 +529,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		else
 		{
 			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, stmt->role,
+			validate_and_get_salt(stmt->role, &salt, &logdetail);
+			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
 			MemSet(new_password_record, 0, sizeof(new_password_record));
 			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
-
+			if (passname != NULL)
+				new_password_record[Anum_pg_auth_password_name - 1] =
+								DirectFunctionCall1(namein, CStringGetDatum(passname));
+			else
+				new_password_record[Anum_pg_auth_password_name - 1] =
+								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -560,6 +642,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* password name for managing multiple passwords */
 	int			connlimit = -1; /* maximum connections allowed */
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
@@ -575,6 +658,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -652,6 +736,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -669,6 +759,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -805,8 +897,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	/* password */
 	if (password)
 	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
+		char	*salt = NULL;
+		const char 	*logdetail = NULL;
+		char	*shadow_pass;
 
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (password[0] == '\0' ||
@@ -818,13 +911,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else
 		{
-			/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
-			new_password_record[Anum_pg_auth_password_password - 1] =
-				CStringGetTextDatum(shadow_pass);
+			if (!validate_and_get_salt(rolename, &salt, &logdetail))
+				new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+			else
+			{
+				/* Encrypt the password to the requested format. */
+				shadow_pass = encrypt_password(Password_encryption, salt,
+											password);
+				new_password_record[Anum_pg_auth_password_password - 1] =
+					CStringGetTextDatum(shadow_pass);
+
+				new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+				if (passname)
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(passname));
+				else
+					new_password_record[Anum_pg_auth_password_name - 1] =
+					DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+			}
 		}
-		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* unset password */
@@ -859,7 +965,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
-		password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid));
+		if (dpassName)
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+		else
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname));
 
 		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
 		{
@@ -1157,14 +1266,23 @@ DropRole(DropRoleStmt *stmt)
 		DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
 
 		/*
-		 * Drop password
+		 * Drop password(s)
 		 */
-		tuple = SearchSysCache1(AUTHPASSWORD, roleid);
-		if (HeapTupleIsValid(tuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self);
-			ReleaseSysCache(tuple);
+		ScanKeyInit(&scankey,
+					Anum_pg_auth_password_roleid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(roleid));
+
+		sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								   true, NULL, 1, &scankey);
+
+		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+		{
+			CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
 		}
 
+		systable_endscan(sscan);
+
 		/*
 		 * Remove settings for this role.
 		 */
@@ -1201,6 +1319,9 @@ RenameRole(const char *oldname, const char *newname)
 				newtuple,
 				passtuple;
 	TupleDesc	dsc;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+
 	Relation	rel;
 	Datum		datum;
 	bool		isnull = true;
@@ -1211,6 +1332,7 @@ RenameRole(const char *oldname, const char *newname)
 	Oid			roleid;
 	ObjectAddress address;
 	Form_pg_authid authform;
+	Relation	pg_auth_password_rel;
 
 	rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	dsc = RelationGetDescr(rel);
@@ -1301,28 +1423,38 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	passtuple = SearchSysCache1(AUTHPASSWORD, roleid);
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_password_roleid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(roleid));
+
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
-	if (HeapTupleIsValid(passtuple))
-		datum = SysCacheGetAttr(AUTHPASSWORD, passtuple,
-								Anum_pg_auth_password_password, &isnull);
+	sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								true, NULL, 1, &scankey);
 
-	if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+	while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
 	{
-		Relation	pg_auth_password_rel;
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+							Anum_pg_auth_password_password, &isnull);
 
-		/* MD5 uses the username as salt, so just clear it on a rename */
-		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
+		{
 
-		if (HeapTupleIsValid(passtuple)) {
-			CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
-			ReleaseSysCache(passtuple);
+			/* MD5 uses the username as salt, so just clear it on a rename */
+
+			if (HeapTupleIsValid(passtuple)) {
+				CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
+				ereport(NOTICE,
+					(errmsg("MD5 password cleared because of role rename")));
+
+			}
 		}
-		table_close(pg_auth_password_rel, NoLock);
-		ereport(NOTICE,
-				(errmsg("MD5 password cleared because of role rename")));
 	}
 
+	systable_endscan(sscan);
+	table_close(pg_auth_password_rel, NoLock);
+
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
 
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index 805b3695b7..652c18fa94 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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,
 			  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);
 
 			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 795f1cba55..7dfc66b64b 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,13 @@ typedef enum
 	SCRAM_AUTH_FINISHED
 } scram_state_enum;
 
+typedef struct
+{
+	uint8		StoredKey[SCRAM_KEY_LEN];
+	uint8		ServerKey[SCRAM_KEY_LEN];
+} scram_secret;
+
+
 typedef struct
 {
 	scram_state_enum state;
@@ -141,10 +148,16 @@ typedef struct
 	Port	   *port;
 	bool		channel_binding_in_use;
 
+	/*
+	 * 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_KEY_LEN];
-	uint8		ServerKey[SCRAM_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;
@@ -220,17 +233,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_auth_password.password.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
  * 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;
@@ -260,46 +276,47 @@ 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->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->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 */
+							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;
 	}
 
 	/*
@@ -310,8 +327,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass)
 	 */
 	if (!got_secret)
 	{
+		state->secrets = palloc0(sizeof(scram_secret));
+
 		mock_scram_secret(state->port->user_name, &state->iterations,
-						  &state->salt, state->StoredKey, state->ServerKey);
+						  &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey);
 		state->doomed = true;
 	}
 
@@ -459,7 +478,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -476,11 +495,13 @@ 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)
+		pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN);
 
 	result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN,
 								SCRAM_DEFAULT_ITERATIONS, password,
@@ -1114,47 +1135,57 @@ verify_client_proof(scram_state *state)
 	uint8		ClientSignature[SCRAM_KEY_LEN];
 	uint8		ClientKey[SCRAM_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_KEY_LEN];
-	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
-	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, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 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(PG_SHA256);
+		elog(LOG, "Trying to verify password %d", j);
+
+		if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 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, sizeof(ClientSignature)) < 0)
+		{
+			elog(LOG, "could not calculate client signature");
+			pg_hmac_free(ctx);
+			continue;
+		}
 
-	pg_hmac_free(ctx);
+		elog(LOG, "success on %d", j);
 
-	/* Extract the ClientKey that the client calculated from the proof */
-	for (i = 0; i < SCRAM_KEY_LEN; 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, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
-		elog(ERROR, "could not hash stored key: %s", errstr);
+		for (i = 0; i < SCRAM_KEY_LEN; i++)
+			ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
 
-	if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0)
-		return false;
+		/* Hash it one more time, and compare with StoredKey */
+		if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0)
+			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	return true;
+		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) {
+			elog(LOG, "Moving forward with Password %d", j);
+			state->chosen_secret = j;
+			return true;
+		}
+	}
+
+	return false;
 }
 
 /*
@@ -1380,7 +1411,7 @@ build_server_final_message(scram_state *state)
 	pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256);
 
 	/* calculate ServerSignature */
-	if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 ||
+	if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 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 269cf286ad..4cc59d39ce 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -58,8 +58,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, const char **logdetail);
 
 
 /*----------------------------------------------------------------
@@ -790,8 +789,9 @@ static int
 CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
-	int			result;
-	char	   *shadow_pass;
+	int			result = STATUS_ERROR;
+	int			i, num;
+	char	   **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -799,17 +799,21 @@ 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)
-	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
+	if (passwords != NULL) {
+		for (i = 0; i < num; 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; i++)
+			pfree(passwords[i]);
+
+		pfree(passwords);
 	}
-	else
-		result = STATUS_ERROR;
 
-	if (shadow_pass)
-		pfree(shadow_pass);
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -824,57 +828,42 @@ 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;
+	char	  **passwords;
 
 	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);
-
-	/*
-	 * 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
-	 * 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)
-		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num);
 
 	/*
 	 * 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 PW 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);
-	else
-		auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,
-									logdetail);
+	if (passwords != NULL) {
+		for (i = 0; i < num; i++)
+			if (get_password_type(passwords[i]) == PASSWORD_TYPE_SCRAM_SHA_256)
+				scram_pw_avail = true;
+
+		if (port->hba->auth_method == uaMD5 && !scram_pw_avail)
+			auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail);
+		else
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
+											logdetail);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
+		for (i = 0; i < num; i++) 
+			pfree(passwords[i]);
 
-	/*
-	 * If get_role_password() returned error, return error, even if the
-	 * authentication succeeded.
-	 */
-	if (!shadow_pass)
-	{
-		Assert(auth_result != STATUS_OK);
-		return STATUS_ERROR;
+		pfree(passwords);
 	}
 
 	if (auth_result == STATUS_OK)
@@ -884,11 +873,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, const char **logdetail)
 {
 	char		md5Salt[4];		/* Password salt */
 	char	   *passwd;
-	int			result;
+	int			result = STATUS_ERROR;
+	int			i;
 
 	if (Db_user_namespace)
 		ereport(FATAL,
@@ -909,12 +899,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; 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 745e61034c..09b6bebf40 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -23,6 +23,7 @@
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
@@ -34,13 +35,15 @@
  * 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)
 {
-	HeapTuple	roleTup, passTup;
+	HeapTuple	roleTup;
 	Datum		datum;
 	bool		isnull;
-	char	   *shadow_pass;
+	char	   **passwords;
+	CatCList   *passlist;
+	int		    i;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,30 +55,34 @@ get_role_password(const char *role, const char **logdetail)
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
-	
-	passTup = SearchSysCache1(AUTHPASSWORD, datum);
+	ReleaseSysCache(roleTup);
+	/* Find any existing password that is not the one being updated to get the salt */
+	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+	*num = passlist->n_members;
 
-	if (!HeapTupleIsValid(passTup))
+	if (passlist->n_members == 0)
 	{
+		*num = 0;
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
+		ReleaseCatCacheList(passlist);
 		return NULL;			/* user has no password */
 	}
-	datum = SysCacheGetAttr(AUTHPASSWORD, passTup,
-							Anum_pg_auth_password_password, &isnull);
 
-	ReleaseSysCache(roleTup);
-	if (isnull) /* this should not happen any more but just in case */
+	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+
+	for (i = 0; i < passlist->n_members; i++)
 	{
-		ReleaseSysCache(passTup);
-		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
-							  role);
-		return NULL;			/* user has no password */
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_password, &isnull);
+		passwords[i] = TextDatumGetCString(datum);
 	}
-	shadow_pass = TextDatumGetCString(datum);
-	ReleaseSysCache(passTup);
 
-	return shadow_pass;
+	ReleaseCatCacheList(passlist);
+
+	return passwords;
 }
 
 /*
@@ -107,7 +114,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);
@@ -128,13 +135,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);
+				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'");
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 969c9c158f..7c1d023b7e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -826,8 +826,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
-	PLACING PLAN PLANS POLICY
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD
+	PATH PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -1261,6 +1261,15 @@ AlterOptRoleElem:
 							 errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
 							 parser_errposition(@1)));
 				}
+			| PASSNAME Sconst
+				{
+					$$ = makeDefElem("passname",
+									 (Node *)makeString($2), @1);
+				}
+			| VALID FOR Sconst
+				{
+					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+				}
 			| INHERIT
 				{
 					$$ = makeDefElem("inherit", (Node *) makeBoolean(true), @1);
@@ -17863,6 +17872,7 @@ unreserved_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PATH
 			| PLAN
@@ -18481,6 +18491,7 @@ bare_label_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PATH
 			| PLACING
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 7c8f864a80..e487cef5b3 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
-		case AUTHPASSWORD:
+		case AUTHPASSWORDNAME:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index de2b3ec1eb..9879120bf7 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -256,12 +256,12 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
-	{AuthPasswordRelationId,			/* AUTHPASSWORD */
+	{AuthPasswordRelationId,			/* AUTHPASSWORDNAME */
 		AuthPasswordRoleOidIndexId,
-		1,
+		2,
 		{
 			Anum_pg_auth_password_roleid,
-			0,
+			Anum_pg_auth_password_name,
 			0,
 			0
 		},
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
index beaa2d40b9..2100cda758 100644
--- a/src/include/catalog/pg_auth_password.h
+++ b/src/include/catalog/pg_auth_password.h
@@ -24,11 +24,14 @@
  *		typedef struct FormData_pg_auth_password
  * ----------------
  */
-CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
 {
 	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+	NameData    name;            /* name of password for multiple password support */
+
 #ifdef CATALOG_VARLEN                /* variable-length fields start here */
-    text            password;        /* password */
+	text            password BKI_FORCE_NOT_NULL;        /* password */
+	timestamptz     expiration BKI_FORCE_NULL;	        /* password expiration time, if any */
 #endif
 } FormData_pg_auth_password;
 
@@ -44,7 +47,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176);
 
 typedef FormData_pg_auth_password *Form_pg_auth_password;
 
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops));
-
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops));
 
 #endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 3e1aae5785..1c14f60a05 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad
 
 extern PGDLLIMPORT check_password_hook_type check_password_hook;
 
-extern bool is_role_valid(const char *rolename, char **logdetail);
+extern bool is_role_valid(const char *rolename, const char **logdetail);
 extern Oid	CreateRole(ParseState *pstate, CreateRoleStmt *stmt);
 extern Oid	AlterRole(ParseState *pstate, AlterRoleStmt *stmt);
 extern Oid	AlterRoleSet(AlterRoleSetStmt *stmt);
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index b8ff8ccb41..84c9c4b4f4 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 39ccf8f0e3..27b4e57033 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, const char **logdetail);
 
 #endif							/* PG_SASL_H */
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index c51e848c24..70b01b511f 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,7 @@
 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, char **salt,
 							   uint8 *stored_key, uint8 *server_key);
 extern bool scram_verify_plain_password(const char *username,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ae35f03251..ec79fafabf 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -334,6 +334,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c7bc648f46..a543c18188 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,7 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
-	AUTHPASSWORD,
+	AUTHPASSWORDNAME,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 4ffc41a545..11fb244021 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -26,7 +26,7 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 -- run. Use a regular expression to mask the changing parts.
 SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
     ORDER BY rolname, password;
@@ -44,7 +44,7 @@ NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
 SELECT rolname, password
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
     ORDER BY rolname, password;
@@ -78,7 +78,7 @@ CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
     ORDER BY rolname, password;
@@ -102,7 +102,7 @@ NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
 SELECT password FROM pg_authid
-LEFT JOIN pg_auth_password p 
+LEFT JOIN pg_auth_password p
 ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
  password 
 ----------
@@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 -- should not contain the original salt.
 SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
@@ -146,7 +146,7 @@ DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
 SELECT rolname, password
     FROM pg_authid
-    LEFT JOIN pg_auth_password p 
+    LEFT JOIN pg_auth_password p
     ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
     ORDER BY rolname, password;
-- 
2.34.1


From 80f2b1948c299cbc799901d36a23d44bee36e00b Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Thu, 30 Jun 2022 18:17:50 -0400
Subject: [PATCH 3/3] Per-password expiration

To build on the multi-password support this
adds per-password expiration, either passed in via ALTER ROLE/CREATE
ROLE with the grammar EXPIRES IN, or via a system-wide setting
called password_valid_duration

Author: Joshua Brindle
---
 src/backend/commands/user.c     | 121 ++++++++++++++++++++++++++++++--
 src/backend/commands/variable.c | 105 +++++++++++++++++++++++++++
 src/backend/libpq/auth.c        |   9 ++-
 src/backend/libpq/crypt.c       |  38 ++++++++--
 src/backend/parser/gram.y       |  10 ++-
 src/backend/utils/misc/guc.c    |  12 ++++
 src/include/commands/user.h     |   3 +
 src/include/commands/variable.h |   4 ++
 src/include/parser/kwlist.h     |   1 +
 9 files changed, 290 insertions(+), 13 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 94747d7c3c..9c46af5123 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -47,8 +47,9 @@
 Oid			binary_upgrade_next_pg_authid_oid = InvalidOid;
 
 
-/* GUC parameter */
+/* GUC parameters */
 int			Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+Interval *default_password_duration = NULL;
 
 /* default password name */
 const char* default_passname = "__def__";
@@ -166,6 +167,84 @@ validate_and_get_salt(char *rolename, char **salt, const char **logdetail)
 	return true;
 }
 
+static
+Datum
+expires_in_datum(DefElem *passExpiresIn)
+{
+	Interval *interval;
+	Node	   *arg;
+	A_Const    *con;
+	TimestampTz now = GetCurrentTimestamp();
+	Datum		passExpiresIn_datum;
+	char *dateout;
+
+	if (default_password_duration != NULL)
+	{
+		/* The default duration GUC is set, use it if nothing came from SQL
+		 * or if something came from SQL, reject it if not from a superuser
+		 */
+
+		if (passExpiresIn != NULL)
+			if (!superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("must be superuser to override password_validity_duration GUC")));
+			else
+				goto bypass;
+		else
+		{
+			passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										 PointerGetDatum(default_password_duration));
+
+			dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+			ereport(NOTICE,
+				(errmsg("Password will expire at: \"%s\" (from GUC)", dateout)));
+
+			return passExpiresIn_datum;
+		}
+	}
+
+	if (passExpiresIn == NULL) 	/* No duration requested via SQL and no system default, no expiration */
+		return PointerGetDatum(NULL);
+
+bypass:
+	arg = (Node *)passExpiresIn->arg;
+	if (IsA(arg, TypeCast))
+	{
+		TypeCast   *tc = (TypeCast *) passExpiresIn->arg;
+		arg = tc->arg;
+	}
+
+	if (!IsA(arg, A_Const))
+	{
+		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
+		return PointerGetDatum(NULL);
+	}
+	con = (A_Const *) arg;
+
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+								CStringGetDatum(strVal(&con->val)),
+								ObjectIdGetDatum(InvalidOid),
+								Int32GetDatum(-1)));
+
+	passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										PointerGetDatum(interval));
+
+	dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+	ereport(NOTICE,
+		(errmsg("Password will expire at: \"%s\" (from SQL)", dateout)));
+
+	return passExpiresIn_datum;
+}
+
 /*
  * CREATE ROLE
  */
@@ -210,7 +289,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
-
+	DefElem    *dpassExpiresIn = NULL;
 
 	/* The defaults can vary depending on the original statement type */
 	switch (stmt->stmt_type)
@@ -319,6 +398,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 
 		else
 			elog(ERROR, "option \"%s\" not recognized",
@@ -507,6 +592,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
 		HeapTuple	new_tuple;
+		Datum		passExpiresIn_datum;
 
 		/*
 		 * Don't allow an empty password. Libpq treats an empty password the
@@ -528,19 +614,27 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
+			MemSet(new_password_record, 0, sizeof(new_password_record));
+			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
+
+			passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+			if (passExpiresIn_datum != PointerGetDatum(NULL))
+				new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+			else
+				new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+
 			/* Encrypt the password to the requested format. */
 			validate_and_get_salt(stmt->role, &salt, &logdetail);
 			shadow_pass = encrypt_password(Password_encryption, salt,
 										   password);
 
-			MemSet(new_password_record, 0, sizeof(new_password_record));
-			MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 			if (passname != NULL)
 				new_password_record[Anum_pg_auth_password_name - 1] =
 								DirectFunctionCall1(namein, CStringGetDatum(passname));
 			else
 				new_password_record[Anum_pg_auth_password_name - 1] =
 								DirectFunctionCall1(namein, CStringGetDatum(default_passname));
+
 			/* open password table and insert it. */
 			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
@@ -647,6 +741,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
 	bool		validUntil_null;
+	Datum		passExpiresIn_datum; /* Time period until password expires */
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
@@ -659,6 +754,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
 	DefElem    *dpassName = NULL;
+	DefElem	   *dpassExpiresIn = NULL;
 	Oid			roleid;
 
 	check_rolespec_name(stmt->role,
@@ -742,6 +838,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dpassName = defel;
 		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -762,6 +864,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (dpassName)
 		passname = strVal(dpassName->arg);
 
+
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
 	 */
@@ -848,6 +951,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls));
 	MemSet(new_password_record_repl, false, sizeof(new_password_record_repl));
 
+	passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+	if (passExpiresIn_datum != PointerGetDatum(NULL)) {
+		new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+		new_password_record_repl[Anum_pg_auth_password_expiration - 1] = true;
+	}
+	else
+		new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
 
 	/*
 	 * issuper/createrole/etc
@@ -957,7 +1067,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ReleaseSysCache(tuple);
 	heap_freetuple(new_tuple);
 
-	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true)
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true
+		|| new_password_record_repl[Anum_pg_auth_password_expiration - 1] == true)
 	{
 		Relation	pg_auth_password_rel;
 		TupleDesc	pg_auth_password_dsc;
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4..ca85a10b80 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "commands/user.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -933,3 +934,107 @@ show_role(void)
 	/* Otherwise we can just use the GUC string */
 	return role_string ? role_string : "none";
 }
+
+
+/*
+ * check_password_duration: GUC check_hook for password_duration
+ */
+bool
+check_password_duration(char **newval, void **extra, GucSource source)
+{
+	Interval	*new_interval;
+	char	   *endptr;
+
+	const char *valueptr = *newval;
+	char	   *val;
+	Interval   *interval;
+
+	if (newval == NULL || *newval == NULL) {
+		extra = NULL;
+		return true;
+	}
+
+	elog(NOTICE,"Setting password duration to \"%s\"",
+					*newval);
+
+	while (isspace((unsigned char) *valueptr))
+		valueptr++;
+	if (*valueptr != '\'') {
+		val = pstrdup(valueptr);
+	}
+	else
+	{
+		valueptr++;
+		val = pstrdup(valueptr);
+		/* Check and remove trailing quote */
+		endptr = strchr(val, '\'');
+		if (!endptr || endptr[1] != '\0')
+		{
+			pfree(val);
+			return false;
+		}
+		*endptr = '\0';
+	}
+
+	/*
+		* Try to parse it.  XXX an invalid interval format will result in
+		* ereport(ERROR), which is not desirable for GUC.  We did what we
+		* could to guard against this in flatten_set_variable_args, but a
+		* string coming in from postgresql.conf might contain anything.
+		*/
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+														CStringGetDatum(val),
+														ObjectIdGetDatum(InvalidOid),
+														Int32GetDatum(-1)));
+
+	pfree(val);
+
+	if (!interval) {
+		return false;
+	}
+
+	new_interval = malloc(sizeof(Interval));
+	memcpy(new_interval, interval, sizeof(Interval));
+	pfree(interval);
+
+	/*
+	 * Pass back data for assign_password_validity to use
+	 */
+	*extra = malloc(sizeof(Interval *));
+	if (!*extra)
+		return false;
+	*((Interval **) *extra) = new_interval;
+
+	return true;
+}
+
+/*
+ * assign_password_validity: GUC assign_hook for timezone
+ */
+void
+assign_password_duration(const char *newval, void *extra)
+{
+	if (extra == NULL)
+		default_password_duration = NULL;
+	else
+		default_password_duration = *((Interval **) extra);
+}
+
+/*
+ * show_password_validity: GUC show_hook for timezone
+ */
+const char *
+show_password_duration(void)
+{
+	const char *intervalout;
+	if (default_password_duration == NULL) {
+		return "";
+	}
+	intervalout = DatumGetCString(DirectFunctionCall1(interval_out,
+										PointerGetDatum(default_password_duration)));
+
+	if (intervalout != NULL)
+		return intervalout;
+
+	return "";
+}
\ No newline at end of file
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 4cc59d39ce..857c58027c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -860,9 +860,14 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num,
 											logdetail);
 
-		for (i = 0; i < num; i++) 
-			pfree(passwords[i]);
+		for (i = 0; i < num; i++) {
 
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(LOG,
+					(errmsg("Password %d was null", i)));
+		}
 		pfree(passwords);
 	}
 
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 09b6bebf40..ae3ca1175f 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -40,10 +40,12 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 {
 	HeapTuple	roleTup;
 	Datum		datum;
+	TimestampTz current, vuntil = 0;
+
 	bool		isnull;
 	char	   **passwords;
 	CatCList   *passlist;
-	int		    i;
+	int		    i, j = 0, num_valid_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -51,6 +53,7 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	{
 		*logdetail = psprintf(_("Role \"%s\" does not exist."),
 							  role);
+		*num = 0;
 		return NULL;			/* no such user */
 	}
 
@@ -58,7 +61,6 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 	ReleaseSysCache(roleTup);
 	/* Find any existing password that is not the one being updated to get the salt */
 	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
-	*num = passlist->n_members;
 
 	if (passlist->n_members == 0)
 	{
@@ -69,15 +71,41 @@ get_role_passwords(const char *role, const char **logdetail, int *num)
 		return NULL;			/* user has no password */
 	}
 
-	passwords = palloc0(sizeof(char *) * passlist->n_members); 
+	current = GetCurrentTimestamp();
+
+	for (i = 0; i < passlist->n_members; i++)
+	{
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+			num_valid_passwords++;
+	}
+
+	passwords = palloc0(sizeof(char *) * num_valid_passwords);
+	*num = num_valid_passwords;
 
 	for (i = 0; i < passlist->n_members; i++)
 	{
 		HeapTuple	tup = &passlist->members[i]->tuple;
 
 		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
-							Anum_pg_auth_password_password, &isnull);
-		passwords[i] = TextDatumGetCString(datum);
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+		{
+			datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+								Anum_pg_auth_password_password, &isnull);
+			passwords[j++] = pstrdup(TextDatumGetCString(datum));
+		}
 	}
 
 	ReleaseCatCacheList(passlist);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7c1d023b7e..1f00a0aeaf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -790,7 +790,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DOUBLE_P DROP
 
 	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
-	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPIRES EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -1266,6 +1266,12 @@ AlterOptRoleElem:
 					$$ = makeDefElem("passname",
 									 (Node *)makeString($2), @1);
 				}
+			| EXPIRES IN_P Sconst opt_interval
+				{
+					TypeName *t = SystemTypeName("interval");
+					t->typmods = $4;
+					$$ = makeDefElem("expiresin", (Node *)makeStringConstCast($3, @3, t), @1);
+				}
 			| VALID FOR Sconst
 				{
 					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
@@ -17766,6 +17772,7 @@ unreserved_keyword:
 			| EXCLUDING
 			| EXCLUSIVE
 			| EXECUTE
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -18340,6 +18347,7 @@ bare_label_keyword:
 			| EXCLUSIVE
 			| EXECUTE
 			| EXISTS
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a7cc49898b..0d4c2c5d79 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -716,6 +716,7 @@ static char *recovery_target_string;
 static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
+static char *password_duration_string;
 
 
 /* should be static, but commands/variable.c needs to get at this */
@@ -4721,6 +4722,17 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"password_valid_duration", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Specifies the default validity duration of new passwords."),
+			NULL,
+			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
+		},
+		&password_duration_string,
+		NULL,
+		check_password_duration, assign_password_duration, show_password_duration
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 1c14f60a05..13d3479fac 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -19,6 +19,9 @@
 /* GUC. Is actually of type PasswordType. */
 extern PGDLLIMPORT int Password_encryption;
 
+/* GUC. system-wide password validity duration */
+extern Interval *default_password_duration;
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/commands/variable.h b/src/include/commands/variable.h
index 0e5ddcbcf3..3b941c554c 100644
--- a/src/include/commands/variable.h
+++ b/src/include/commands/variable.h
@@ -34,5 +34,9 @@ extern void assign_session_authorization(const char *newval, void *extra);
 extern bool check_role(char **newval, void **extra, GucSource source);
 extern void assign_role(const char *newval, void *extra);
 extern const char *show_role(void);
+extern bool check_password_duration(char **newval, void **extra, GucSource source);
+extern void assign_password_duration(const char *newval, void *extra);
+extern const char *show_password_duration(void);
+
 
 #endif							/* VARIABLE_H */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ec79fafabf..cebf01840b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -163,6 +163,7 @@ PG_KEYWORD("excluding", EXCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exclusive", EXCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("execute", EXECUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exists", EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("expires", EXPIRES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("explain", EXPLAIN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("expression", EXPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("extension", EXTENSION, UNRESERVED_KEYWORD, BARE_LABEL)
-- 
2.34.1

#9Brindle, Joshua
joshuqbr@amazon.com
In reply to: Stephen Frost (#8)
Re: [PoC/RFC] Multiple passwords, interval expirations

On 6/30/22 8:20 PM, Stephen Frost wrote:

Greetings,

* Gurjeet Singh (gurjeet@singh.im) wrote:

I am planning on picking it up next week; right now picking up steam,
and reviewing a different, smaller patch.

Great! Glad that others are interested in this.

At his behest, I had a conversation with Joshua (OP), and have his
support to pick up and continue working on this patch. I have a some
ideas of my own, on what this patch should do, but since I haven't
fully reviewed the (bulky) patch, I'll reserve my proposals until I
wrap my head around it.

I'd be curious as to your thought as to what the patch should be doing.
Joshua and I had discussed it at some length as he was working on it.

Adding myself to the CC list here /waves

I gave Gurjeet a bit of a brain dump on what I had planned (and what
we'd talked about), though he's free to take it in a different direction
if he wants.

Please expect some activity on this patch towards the end of next week.

I've gone ahead and updated it, cleaned up a couple things, and make it
so that check-world actually passes with it. Attached is an updated
version and I'll add it to the July commitfest.

Ah, thanks. Hopefully it wasn't too horrible of a rebase.

Show quoted text

Thanks!

Stephen

#10Stephen Frost
sfrost@snowman.net
In reply to: Brindle, Joshua (#9)
Re: [PoC/RFC] Multiple passwords, interval expirations

Greetings,

On Fri, Jul 1, 2022 at 10:51 Brindle, Joshua <joshuqbr@amazon.com> wrote:

On 6/30/22 8:20 PM, Stephen Frost wrote:

* Gurjeet Singh (gurjeet@singh.im) wrote:

I am planning on picking it up next week; right now picking up steam,
and reviewing a different, smaller patch.

Great! Glad that others are interested in this.

At his behest, I had a conversation with Joshua (OP), and have his
support to pick up and continue working on this patch. I have a some
ideas of my own, on what this patch should do, but since I haven't
fully reviewed the (bulky) patch, I'll reserve my proposals until I
wrap my head around it.

I'd be curious as to your thought as to what the patch should be doing.
Joshua and I had discussed it at some length as he was working on it.

Adding myself to the CC list here /waves

Hi!

I gave Gurjeet a bit of a brain dump on what I had planned (and what

we'd talked about), though he's free to take it in a different direction
if he wants.

Perhaps though would certainly like this to patch to be useful for the
use-cases that we had discussed, naturally. :)

Please expect some activity on this patch towards the end of next week.
I've gone ahead and updated it, cleaned up a couple things, and make it
so that check-world actually passes with it. Attached is an updated
version and I'll add it to the July commitfest.

Ah, thanks. Hopefully it wasn't too horrible of a rebase.

Wasn’t too bad.. needs more clean-up, there was some white space issues and
some simple re-base stuff, but then the support for “md5” pg_hba option was
broken for users with SCRAM passwords because we weren’t checking if there
was a SCRAM pw stored and upgrading to SCRAM in that case. That’s the main
case that I fixed. We will need to document this though, of course. The
patch I submitted should basically do:

pg_hba md5 + md5-only pws -> md5 auth used
pg_hba md5 + scram-only pws -> scram
pg_hba md5 + md5 and scram pws -> scram
pg_hba scram -> scram

Not sure if we need to try and do something to make it possible to have
pg_hba md5 + mixed pws and have md5 used but it’s tricky as we would have
to know on the server side early on if that’s what we want to do. We could
add an option to md5 to say “only do md5” maybe but I’m also inclined to
not bother and tell people to just get moved to scram already.

For my 2c, I’d also like to move to having a separate column for the PW
type from the actual secret but that’s largely an independent change.

Thanks!

Stephen

Show quoted text
#11Gurjeet Singh
gurjeet@singh.im
In reply to: Stephen Frost (#10)
1 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Fri, Jul 1, 2022 at 8:13 AM Stephen Frost <sfrost@snowman.net> wrote:

On Fri, Jul 1, 2022 at 10:51 Brindle, Joshua <joshuqbr@amazon.com> wrote:

On 6/30/22 8:20 PM, Stephen Frost wrote:

I've gone ahead and updated it, cleaned up a couple things, and make it
so that check-world actually passes with it. Attached is an updated
version and I'll add it to the July commitfest.

Ah, thanks. Hopefully it wasn't too horrible of a rebase.

Wasn’t too bad.. needs more clean-up, there was some white space issues and some simple re-base stuff, but then the support for “md5” pg_hba option was broken for users with SCRAM passwords because we weren’t checking if there was a SCRAM pw stored and upgrading to SCRAM in that case. That’s the main case that I fixed. We will need to document this though, of course. The patch I submitted should basically do:

pg_hba md5 + md5-only pws -> md5 auth used
pg_hba md5 + scram-only pws -> scram
pg_hba md5 + md5 and scram pws -> scram
pg_hba scram -> scram

Not sure if we need to try and do something to make it possible to have pg_hba md5 + mixed pws and have md5 used but it’s tricky as we would have to know on the server side early on if that’s what we want to do. We could add an option to md5 to say “only do md5” maybe but I’m also inclined to not bother and tell people to just get moved to scram already.

For my 2c, I’d also like to move to having a separate column for the PW type from the actual secret but that’s largely an independent change.

The docs say this about rolpassword in case it stores SCRAM-SHA-256
encrypted password "If the password is encrypted with SCRAM-SHA-256,
it has the format SCRAM-SHA-256$... This format is the same as that
specified by RFC-5803". So I believe our hands are tied, and we
cannot change that without breaking compliance with RFC 5803.

Please see attached v4 of the patch. The patch takes care of rebase to
the master/17-devel branch, and includes some changes, too. The
rebase/merge conflicts were quite involved, since some affected files
had been removed, or even split into multiple files over the course of
the last year; resolving merge-conflicts was more of a grunt work.

The changes since V3 are (compare [1]v3 patch, applied to a contemporary commit on master branch https://github.com/gurjeet/postgres/commits/multiple_passwords_v3 vs. [2]main development branch, patch rebased to current master branch, followed by many changes https://github.com/gurjeet/postgres/commits/multiple_passwords, Git branches linked below):
- Remove TOAST table and corresponding index from pg_authid.
- Fix memory leak/allocation bug; replace malloc() with guc_alloc().
- Fix assumptions about passed-in double-pointers to GUC handling functions.
- Remove the new function is_role_valid() and its call sites, because
I believe it made backward-incompatible change to authentication
behavior (see more below).
- Improve error handling that was missing at a few places.
- Remove unnecessary checks, like (*var != NULL) checks when we know
all callers pass a NULL by convention.
- Replace MemSet() calls with var={0} styled initialization.
- Minor edits to docs to change them from pg_authid to pg_auth_password.

More details about why I chose to remove is_role_valid() and revert to
the old code:
is_role_valid() was a new function that pulled out a small section of
code from get_role_passwords() . I don't think moving this code block
to a new function gains us anything; in fact, it now forces us to call
the new function in two new locations, which we didn't have to do
before. It has to throw the same error messages as before, to maintain
compatibility with external tools/libraries, hence it duplicates those
messages as well, which is not ideal.

Moreover, before the patch, in case of CheckPasswordAuth(), the error
(if any) would have been thrown _after_ network communication done by
sendAuthRequest() call. But with this patch, the error is thrown
before the network interaction, hence this changes the order of
network interaction and the error message. This may have security
implications, too, but I'm unable to articulate one right now.

If we really want the role-validity check to be a function of its own,
a separate patch can address that; this patch doesn't have to make
that decision.

Open question: If a client is capable of providing just md5 passwords
handshake, and because of pg_hba.conf setting, or because the role has
at least one SCRAM password (essentially the 3rd case you mention
above: pg_hba md5 + md5 and scram pws -> scram), the server will
respond with a SASL/SCRAM authentication response, and that would
break the backwards compatibility and will deny access to the client.
Does this make it necessary to use a newer libpq/client library?

Before the patch, the rolvaliduntil was used to check and complain
that the password has expired, as the docs explicitly state that
rolvaliduntil represents "Password expiry time (only used for password
authentication); null if no expiration" . Keeping that column after
the introduction of per-password expiry times now separates the
role-validity from password validity. During an internal discussion a
curiosity arose whether we can simply remove rolvaliduntil. And I
believe the answer is yes, primarily because of how the docs describe
the column. So my proposal is to remove rolvaliduntil from pg_authid,
and on a case-by-case basis, optionally replace its uses with
max(pg_auth_password.expiration).

Comments?

Next steps:
- Break the patch into smaller patches.
- Address TODO items
- Comment each new function
- Add tests
- Add/update documentation

PS: Since this is a large patch, and because in some portions the code
has been indented by a level or two (e.g. to run a `for` loop over
existing code for single-password), I have found the following Git
command to be helpful in reviewing the changes between master and this
branch,: `git diff -b --color-words -U20 origin/master...HEAD -- `

[1]: v3 patch, applied to a contemporary commit on master branch https://github.com/gurjeet/postgres/commits/multiple_passwords_v3
https://github.com/gurjeet/postgres/commits/multiple_passwords_v3

[2]: main development branch, patch rebased to current master branch, followed by many changes https://github.com/gurjeet/postgres/commits/multiple_passwords
followed by many changes
https://github.com/gurjeet/postgres/commits/multiple_passwords

Best regards,
Gurjeet
http://Gurje.et

Attachments:

multiple_passwords.v4.diffapplication/octet-stream; name=multiple_passwords.v4.diffDownload
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index a72f80f033..babaae69e4 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1281,7 +1281,7 @@ omicron         bryanh                  guest1
    <para>
     <productname>PostgreSQL</productname> database passwords are
     separate from operating system user passwords. The password for
-    each database user is stored in the <literal>pg_authid</literal> system
+    each database user is stored in the <literal>pg_passwords</literal> system
     catalog. Passwords can be managed with the SQL commands
     <xref linkend="sql-createrole"/> and
     <xref linkend="sql-alterrole"/>,
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 3e9994793d..8a377d3f23 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -118,7 +118,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_auth_password.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 1bf6c5633c..2e8e37b59b 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -28,6 +28,7 @@
 #include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
@@ -246,6 +247,7 @@ IsSharedRelation(Oid relationId)
 {
 	/* These are the shared catalogs (look for BKI_SHARED_RELATION) */
 	if (relationId == AuthIdRelationId ||
+		relationId == AuthPasswordRelationId ||
 		relationId == AuthMemRelationId ||
 		relationId == DatabaseRelationId ||
 		relationId == DbRoleSettingRelationId ||
@@ -260,6 +262,8 @@ IsSharedRelation(Oid relationId)
 	/* These are their indexes */
 	if (relationId == AuthIdOidIndexId ||
 		relationId == AuthIdRolnameIndexId ||
+		relationId == AuthPasswordRoleOidIndexId ||
+		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemMemRoleIndexId ||
 		relationId == AuthMemRoleMemIndexId ||
 		relationId == AuthMemOidIndexId ||
@@ -281,8 +285,8 @@ IsSharedRelation(Oid relationId)
 		relationId == TablespaceOidIndexId)
 		return true;
 	/* These are their toast tables and toast indexes */
-	if (relationId == PgAuthidToastTable ||
-		relationId == PgAuthidToastIndex ||
+	if (relationId == PgAuthPasswordToastTable ||
+		relationId == PgAuthPasswordToastIndex ||
 		relationId == PgDatabaseToastTable ||
 		relationId == PgDatabaseToastIndex ||
 		relationId == PgDbRoleSettingToastTable ||
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 77b06e2a7a..2c9e074607 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -40,11 +40,13 @@ CREATE VIEW pg_shadow AS
         rolsuper AS usesuper,
         rolreplication AS userepl,
         rolbypassrls AS usebypassrls,
-        rolpassword AS passwd,
+        p.password AS passwd,
         rolvaliduntil AS valuntil,
         setconfig AS useconfig
     FROM pg_authid LEFT JOIN pg_db_role_setting s
     ON (pg_authid.oid = setrole AND setdatabase = 0)
+    LEFT JOIN pg_auth_password p
+    ON (p.roleid = pg_authid.oid)
     WHERE rolcanlogin;
 
 REVOKE ALL ON pg_shadow FROM public;
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index ce77a055e5..8bdd0eb592 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -23,6 +23,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "commands/comment.h"
@@ -30,7 +31,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"
@@ -88,6 +91,11 @@ char	   *createrole_self_grant = "";
 bool		createrole_self_grant_enabled = false;
 GrantRoleOptions createrole_self_grant_options;
 
+Interval *default_password_duration = NULL;
+
+/* default password name */
+static const char* default_password_name = "__def__";
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 check_password_hook_type check_password_hook = NULL;
 
@@ -125,6 +133,164 @@ have_createrole_privilege(void)
 	return has_createrole_privilege(GetUserId());
 }
 
+static bool
+validate_and_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 one 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]);
+	pfree(current_secrets);
+
+	if (salt2)
+		*salt = pstrdup(salt2);
+
+	return true;
+}
+
+static Datum
+expires_in_datum(DefElem *passExpiresIn)
+{
+	Interval *interval;
+	Node	   *arg;
+	A_Const    *con;
+	TimestampTz now = GetCurrentTimestamp();
+	Datum		passExpiresIn_datum;
+	char *dateout;
+
+	if (default_password_duration != NULL)
+	{
+		/*
+		 * The default duration GUC is set, use it if nothing came from SQL.
+		 * If something came from SQL, reject it unless it is from a superuser.
+		 */
+		if (passExpiresIn != NULL)
+		{
+			if (!superuser())
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("must be superuser to override password_validity_duration GUC")));
+			else
+				goto bypass;
+		}
+		else
+		{
+			passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										 PointerGetDatum(default_password_duration));
+
+			dateout =
+				DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+			/* TODO: convert to DEBUG2 */
+			ereport(NOTICE,
+				(errmsg("Password will expire at: \"%s\" (from GUC)", dateout)));
+
+			return passExpiresIn_datum;
+		}
+	}
+
+	if (passExpiresIn == NULL) 	/* No duration requested via SQL and no system default, no expiration */
+		return PointerGetDatum(NULL);
+
+bypass:
+	arg = (Node *)passExpiresIn->arg;
+	if (IsA(arg, TypeCast)) // TODO: Do we really need to do this? Ensure a test-case covers or obviates it.
+	{
+		TypeCast   *tc = (TypeCast *) passExpiresIn->arg;
+		arg = tc->arg;
+	}
+
+	if (!IsA(arg, A_Const))
+	{
+		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg));
+		/* Keep compiler happy */
+		return PointerGetDatum(NULL);
+	}
+
+	con = (A_Const *) arg;
+	if (con->isnull || !IsA(&(con->val), String))
+	{
+		elog(ERROR, "'expires in' value must be a string");
+		/* Keep compiler happy */
+		return PointerGetDatum(NULL);
+	}
+
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+								CStringGetDatum(strVal(&con->val)),
+								ObjectIdGetDatum(InvalidOid),
+								Int32GetDatum(-1)));
+
+	passExpiresIn_datum =  DirectFunctionCall2(timestamptz_pl_interval,
+										TimestampGetDatum(now),
+										PointerGetDatum(interval));
+
+	dateout = DatumGetCString(DirectFunctionCall1(timestamp_out,
+													passExpiresIn_datum));
+
+	ereport(NOTICE,
+		(errmsg("Password will expire at: \"%s\" (from SQL)", dateout)));
+
+	return passExpiresIn_datum;
+}
 
 /*
  * CREATE ROLE
@@ -142,6 +308,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	ListCell   *item;
 	ListCell   *option;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* name of the password for managing multiple passwords */
 	bool		issuper = false;	/* Make the user a superuser? */
 	bool		inherit = true; /* Auto inherit privileges? */
 	bool		createrole = false; /* Can this user create roles? */
@@ -169,6 +336,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	DefElem    *dadminmembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+	DefElem    *dpassExpiresIn = NULL;
 	GrantRoleOptions popt;
 
 	/* The defaults can vary depending on the original statement type */
@@ -272,6 +441,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
+
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -309,6 +491,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		validUntil = strVal(dvalidUntil->arg);
 	if (dbypassRLS)
 		bypassrls = boolVal(dbypassRLS->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/* Check some permissions first */
 	if (!superuser_arg(currentUserId))
@@ -415,43 +599,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
 	new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
 	new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
-
-	if (password)
-	{
-		char	   *shadow_pass;
-		const char *logdetail = NULL;
-
-		/*
-		 * Don't allow an empty password. Libpq treats an empty password the
-		 * same as no password at all, and won't even try to authenticate. But
-		 * other clients might, so allowing it would be confusing. By clearing
-		 * the password when an empty string is specified, the account is
-		 * consistently locked for all clients.
-		 *
-		 * Note that this only covers passwords stored in the database itself.
-		 * There are also checks in the authentication code, to forbid an
-		 * empty password from being used with authentication methods that
-		 * fetch the password from an external system, like LDAP or PAM.
-		 */
-		if (password[0] == '\0' ||
-			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
-		{
-			ereport(NOTICE,
-					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-		}
-		else
-		{
-			/* 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);
-		}
-	}
-	else
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
-
 	new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
 	new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
 
@@ -486,6 +633,74 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	 */
 	CatalogTupleInsert(pg_authid_rel, tuple);
 
+	if (password)
+	{
+		char	   *shadow_pass, *salt;
+		const char	   *logdetail;
+		Datum		new_password_record[Natts_pg_auth_password] = {0};
+		bool		new_password_record_nulls[Natts_pg_auth_password] = {0};
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple	new_tuple;
+		Datum		passExpiresIn_datum;
+
+		/*
+		 * Don't allow an empty password. Libpq treats an empty password the
+		 * same as no password at all, and won't even try to authenticate. But
+		 * other clients might, so allowing it would be confusing. By clearing
+		 * the password when an empty string is specified, the account is
+		 * consistently locked for all clients.
+		 *
+		 * Note that this only covers passwords stored in the database itself.
+		 * There are also checks in the authentication code, to forbid an
+		 * empty password from being used with authentication methods that
+		 * fetch the password from an external system, like LDAP or PAM.
+		 */
+		if (password[0] == '\0' ||
+			plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
+		{
+			ereport(NOTICE,
+					(errmsg("empty string is not a valid password, clearing password")));
+		}
+		else
+		{
+			passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+			if (passExpiresIn_datum != PointerGetDatum(NULL))
+				new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+			else
+				new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+
+			/* Encrypt the password to the requested format. */
+			if (!validate_and_get_salt(stmt->role, &salt, &logdetail))
+				ereport(ERROR,
+						(errmsg("could not get a valid salt for password"),
+						errdetail("%s", logdetail)));
+
+			shadow_pass = encrypt_password(Password_encryption, salt, password);
+
+			if (passname != NULL)
+				new_password_record[Anum_pg_auth_password_name - 1] =
+								DirectFunctionCall1(namein, CStringGetDatum(passname));
+			else
+				new_password_record[Anum_pg_auth_password_name - 1] =
+								DirectFunctionCall1(namein, CStringGetDatum(default_password_name));
+
+			/* open password table and insert it. */
+			pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+			pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+
+			new_password_record[Anum_pg_auth_password_password - 1] =
+				CStringGetTextDatum(shadow_pass);
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+			table_close(pg_auth_password_rel, NoLock);
+		}
+	}
+
 	/*
 	 * Advance command counter so we can see new record; else tests in
 	 * AddRoleMems may fail.
@@ -622,6 +837,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	Datum		new_record[Natts_pg_authid] = {0};
 	bool		new_record_nulls[Natts_pg_authid] = {0};
 	bool		new_record_repl[Natts_pg_authid] = {0};
+	Datum		new_password_record[Natts_pg_auth_password] = {0};
+	bool		new_password_record_nulls[Natts_pg_auth_password] = {0};
+	bool		new_password_record_repl[Natts_pg_auth_password] = {0};
 	Relation	pg_authid_rel;
 	TupleDesc	pg_authid_dsc;
 	HeapTuple	tuple,
@@ -630,10 +848,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
+	char       *passname = NULL;    /* password name for managing multiple passwords */
 	int			connlimit = -1; /* maximum connections allowed */
 	char	   *validUntil = NULL;	/* time the login is valid until */
 	Datum		validUntil_datum;	/* same, as timestamptz Datum */
 	bool		validUntil_null;
+	Datum		passExpiresIn_datum; /* Time period until password expires */
 	DefElem    *dpassword = NULL;
 	DefElem    *dissuper = NULL;
 	DefElem    *dinherit = NULL;
@@ -645,6 +865,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	DefElem    *drolemembers = NULL;
 	DefElem    *dvalidUntil = NULL;
 	DefElem    *dbypassRLS = NULL;
+	DefElem    *dpassName = NULL;
+	DefElem	   *dpassExpiresIn = NULL;
 	Oid			roleid;
 	Oid			currentUserId = GetUserId();
 	GrantRoleOptions popt;
@@ -724,6 +946,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dbypassRLS = defel;
 		}
+		else if (strcmp(defel->defname, "passname") == 0)
+		{
+			if (dpassName)
+				errorConflictingDefElem(defel, pstate);
+			dpassName = defel;
+		}
+		else if (strcmp(defel->defname, "expiresin") == 0)
+		{
+			if (dpassExpiresIn)
+				errorConflictingDefElem(defel, pstate);
+			dpassExpiresIn = defel;
+		}
 		else
 			elog(ERROR, "option \"%s\" not recognized",
 				 defel->defname);
@@ -741,6 +975,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 	if (dvalidUntil)
 		validUntil = strVal(dvalidUntil->arg);
+	if (dpassName)
+		passname = strVal(dpassName->arg);
 
 	/*
 	 * Scan the pg_authid relation to be certain the user exists.
@@ -857,6 +1093,15 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	 * Build an updated tuple, perusing the information just obtained
 	 */
 
+	passExpiresIn_datum = expires_in_datum(dpassExpiresIn);
+	if (passExpiresIn_datum != PointerGetDatum(NULL))
+	{
+		new_password_record[Anum_pg_auth_password_expiration - 1] = passExpiresIn_datum;
+		new_password_record_repl[Anum_pg_auth_password_expiration - 1] = true;
+	}
+	else
+		new_password_record_nulls[Anum_pg_auth_password_expiration - 1] = true;
+
 	/*
 	 * issuper/createrole/etc
 	 */
@@ -915,6 +1160,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (password)
 	{
 		char	*shadow_pass;
+		char	*salt = NULL;
 		const char 	*logdetail = NULL;
 
 		/* Like in CREATE USER, don't allow an empty password. */
@@ -923,24 +1169,41 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		{
 			ereport(NOTICE,
 					(errmsg("empty string is not a valid password, clearing password")));
-			new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+			new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 		}
 		else
+		{
+			if (!validate_and_get_salt(rolename, &salt, &logdetail))
+				ereport(ERROR,
+						(errmsg("could not get a valid salt for password"),
+						errdetail("%s", logdetail)));
+
+			if (salt == NULL)
+				new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
+			else
 			{
 				/* Encrypt the password to the requested format. */
-			shadow_pass = encrypt_password(Password_encryption, rolename,
-										   password);
-			new_record[Anum_pg_authid_rolpassword - 1] =
+				shadow_pass = encrypt_password(Password_encryption, salt, password);
+				new_password_record[Anum_pg_auth_password_password - 1] =
 						CStringGetTextDatum(shadow_pass);
+
+				new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+
+				if (passname)
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(passname));
+				else
+					new_password_record[Anum_pg_auth_password_name - 1] =
+						DirectFunctionCall1(namein, CStringGetDatum(default_password_name));
+			}
 		}
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
 	}
 
 	/* unset password */
 	if (dpassword && dpassword->arg == NULL)
 	{
-		new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
-		new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
+		new_password_record_repl[Anum_pg_auth_password_password - 1] = true;
+		new_password_record_nulls[Anum_pg_auth_password_password - 1] = true;
 	}
 
 	/* valid until */
@@ -957,11 +1220,52 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
 								  new_record_nulls, new_record_repl);
 	CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
+	ReleaseSysCache(tuple);
+	heap_freetuple(new_tuple);
 
-	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
+	if (new_password_record_repl[Anum_pg_auth_password_password - 1] == true
+		|| new_password_record_repl[Anum_pg_auth_password_expiration - 1] == true)
+	{
+		Relation	pg_auth_password_rel;
+		TupleDesc	pg_auth_password_dsc;
+		HeapTuple   password_tuple;
 
-	ReleaseSysCache(tuple);
+		pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+		pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel);
+		if (dpassName)
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname));
+		else
+			password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_password_name));
+
+		if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */
+		{
+			if (HeapTupleIsValid(password_tuple))
+			{
+				CatalogTupleDelete(pg_auth_password_rel, &password_tuple->t_self);
+				ReleaseSysCache(password_tuple);
+			}
+		}
+		else if (HeapTupleIsValid(password_tuple)) /* update existing password */
+		{
+			new_tuple = heap_modify_tuple(password_tuple, pg_auth_password_dsc, new_password_record,
+										new_password_record_nulls, new_password_record_repl);
+			CatalogTupleUpdate(pg_auth_password_rel, &password_tuple->t_self, new_tuple);
+			ReleaseSysCache(password_tuple);
 			heap_freetuple(new_tuple);
+		}
+		else /* insert new password */
+		{
+			new_password_record[Anum_pg_auth_password_roleid - 1] = roleid;
+
+			new_tuple = heap_form_tuple(pg_auth_password_dsc,
+										new_password_record, new_password_record_nulls);
+			CatalogTupleInsert(pg_auth_password_rel, new_tuple);
+			heap_freetuple(new_tuple);
+		}
+
+		table_close(pg_auth_password_rel, NoLock);
+	}
+	InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
 
 	InitGrantRoleOptions(&popt);
 
@@ -1083,7 +1387,6 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
 	return roleid;
 }
 
-
 /*
  * DROP ROLE
  */
@@ -1091,7 +1394,8 @@ void
 DropRole(DropRoleStmt *stmt)
 {
 	Relation	pg_authid_rel,
-				pg_auth_members_rel;
+				pg_auth_members_rel,
+				pg_auth_password_rel;
 	ListCell   *item;
 	List	   *role_addresses = NIL;
 
@@ -1108,6 +1412,7 @@ DropRole(DropRoleStmt *stmt)
 	 */
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
 
 	foreach(item, stmt->roles)
 	{
@@ -1249,6 +1554,29 @@ DropRole(DropRoleStmt *stmt)
 
 		systable_endscan(sscan);
 
+		/*
+		 * Drop password(s)
+		 */
+		ScanKeyInit(&scankey,
+					Anum_pg_auth_password_roleid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(roleid));
+
+		sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								   true, NULL, 1, &scankey);
+
+		while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
+		{
+			CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self);
+		}
+
+		systable_endscan(sscan);
+
+		/*
+		 * Remove settings for this role.
+		 */
+		DropSetting(InvalidOid, roleid);
+
 		/*
 		 * Advance command counter so that later iterations of this loop will
 		 * see the changes already made.  This is essential if, for example,
@@ -1331,6 +1659,7 @@ DropRole(DropRoleStmt *stmt)
 	 * Now we can clean up; but keep locks until commit.
 	 */
 	table_close(pg_auth_members_rel, NoLock);
+	table_close(pg_auth_password_rel, NoLock);
 	table_close(pg_authid_rel, NoLock);
 }
 
@@ -1341,11 +1670,15 @@ ObjectAddress
 RenameRole(const char *oldname, const char *newname)
 {
 	HeapTuple	oldtuple,
-				newtuple;
+				newtuple,
+				passtuple;
 	TupleDesc	dsc;
+	ScanKeyData scankey;
+	SysScanDesc sscan;
+
 	Relation	rel;
 	Datum		datum;
-	bool		isnull;
+	bool		isnull = true;
 	Datum		repl_val[Natts_pg_authid];
 	bool		repl_null[Natts_pg_authid];
 	bool		repl_repl[Natts_pg_authid];
@@ -1353,6 +1686,7 @@ RenameRole(const char *oldname, const char *newname)
 	Oid			roleid;
 	ObjectAddress address;
 	Form_pg_authid authform;
+	Relation	pg_auth_password_rel;
 
 	rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	dsc = RelationGetDescr(rel);
@@ -1449,17 +1783,38 @@ RenameRole(const char *oldname, const char *newname)
 															   CStringGetDatum(newname));
 	repl_null[Anum_pg_authid_rolname - 1] = false;
 
-	datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
+	ScanKeyInit(&scankey,
+				Anum_pg_auth_password_roleid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(roleid));
+
+	pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock);
+
+	sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId,
+								true, NULL, 1, &scankey);
+
+	while (HeapTupleIsValid(passtuple = systable_getnext(sscan)))
+	{
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple,
+							Anum_pg_auth_password_password, &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_rolpassword - 1] = true;
-		repl_null[Anum_pg_authid_rolpassword - 1] = true;
 
+			if (HeapTupleIsValid(passtuple))
+			{
+				CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self);
 				ereport(NOTICE,
 					(errmsg("MD5 password cleared because of role rename")));
+
+			}
 		}
+	}
+
+	systable_endscan(sscan);
+	table_close(pg_auth_password_rel, NoLock);
 
 	newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
 	CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index a88cf5f118..c9b9bda5fd 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
+#include "commands/user.h"
 #include "common/string.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
@@ -1234,3 +1235,100 @@ check_ssl(bool *newval, void **extra, GucSource source)
 #endif
 	return true;
 }
+
+/*
+ * check_password_duration: GUC check_hook for password_duration
+ */
+bool
+check_password_duration(char **newval, void **extra, GucSource source)
+{
+	Interval	*new_interval;
+	char	   *endptr;
+
+	const char *valueptr = *newval;
+	char	   *val;
+	Interval   *interval;
+
+	if (*newval == NULL)
+		return true;
+
+	/* TODO: Change to DEBUG2 */
+	elog(NOTICE,"Setting password duration to \"%s\"", *newval);
+
+	while (isspace((unsigned char) *valueptr))
+		valueptr++;
+
+	if (*valueptr != '\'')
+	{
+		val = pstrdup(valueptr);
+	}
+	else
+	{
+		valueptr++;
+		val = pstrdup(valueptr);
+		/* Check and remove trailing quote */
+		endptr = strchr(val, '\'');
+		if (!endptr || endptr[1] != '\0')
+		{
+			pfree(val);
+			return false;
+		}
+		*endptr = '\0';
+	}
+
+	/*
+		* Try to parse it.  XXX an invalid interval format will result in
+		* ereport(ERROR), which is not desirable for GUC.  We did what we
+		* could to guard against this in flatten_set_variable_args, but a
+		* string coming in from postgresql.conf might contain anything.
+		*/
+	interval = DatumGetIntervalP(DirectFunctionCall3(interval_in,
+														CStringGetDatum(val),
+														ObjectIdGetDatum(InvalidOid),
+														Int32GetDatum(-1)));
+
+	pfree(val);
+
+	if (!interval)
+		return false;
+
+	new_interval = guc_malloc(LOG, sizeof(Interval));
+	memcpy(new_interval, interval, sizeof(Interval));
+	pfree(interval);
+
+	*extra = (void*) new_interval;
+
+	return true;
+}
+
+/*
+ * assign_password_validity: GUC assign_hook for password_duration
+ */
+void
+assign_password_duration(const char *newval, void *extra)
+{
+	if (extra == NULL)
+		default_password_duration = NULL;
+	else
+		default_password_duration = (Interval *) extra;
+}
+
+/*
+ * show_password_validity: GUC show_hook for password_duration
+ */
+const char *
+show_password_duration(void)
+{
+	const char *intervalout;
+
+	if (default_password_duration == NULL)
+		return "";
+
+	intervalout = DatumGetCString(DirectFunctionCall1(interval_out,
+										PointerGetDatum(default_password_duration)));
+
+	if (intervalout != NULL)
+		return intervalout;
+
+	return "";
+}
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index c535bc5383..fa03615696 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -33,7 +33,7 @@
  * implementation.
  *
  * shadow_pass is an optional pointer to the stored secret of the role
- * authenticated, from pg_authid.rolpassword.  For mechanisms that use
+ * authenticated, from pg_auth_password.password.  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.
@@ -49,7 +49,7 @@
  * 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 118d15b1a1..9c66d09206 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,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;
@@ -145,10 +151,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;
@@ -231,17 +243,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.
+ * 'passwords' are the role's stored secrets, from pg_auth_password.password.
  * 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;
@@ -270,49 +285,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);
+		state->secrets = palloc0(sizeof(scram_secret) * num_secrets);
+		state->num_secrets = num_secrets;
+		for (i = 0; i < num_secrets; i++)
+		{
+			int			password_type = get_password_type(secrets[i]);
 
 			if (password_type == PASSWORD_TYPE_SCRAM_SHA_256)
 			{
-			if (parse_scram_secret(shadow_pass, &state->iterations,
+				if (parse_scram_secret(secrets[i], &state->iterations,
 									   &state->hash_type, &state->key_length,
 									   &state->salt,
-								   state->StoredKey,
-								   state->ServerKey))
-				got_secret = true;
-			else
+									   state->secrets[i].StoredKey,
+									   state->secrets[i].ServerKey))
 				{
-				/*
-				 * The password looked like a SCRAM secret, but could not be
-				 * parsed.
-				 */
-				ereport(LOG,
-						(errmsg("invalid SCRAM secret for user \"%s\"",
+					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;
+							got_secret = false; /* fail and allow mock creditials to be created */
+							pfree(state->secrets);
+							state->num_secrets = 0;
+							break;
 						}
 					}
 					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;
+						salt = state->salt;
+						iterations = state->iterations;
+						got_secret = true; /* We got at least one good SCRAM secret */
 					}
 				}
 				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.
+					* The password looked like a SCRAM secret, but could not be
+					* parsed.
 					*/
-		got_secret = false;
+					ereport(LOG,
+							(errmsg("invalid SCRAM secret for user \"%s\"",
+									state->port->user_name)));
+				}
+			}
+		}
 	}
 
 	/*
@@ -323,10 +343,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;
 	}
 
@@ -469,12 +492,12 @@ 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_auth_password.password.
  *
  * 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;
@@ -491,11 +514,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,
@@ -1142,16 +1174,20 @@ 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 ||
+	for (j = 0; j < state->num_secrets; j++)
+	{
+		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 ||
@@ -1165,10 +1201,15 @@ verify_client_proof(scram_state *state)
 						strlen(state->client_final_message_without_proof)) < 0 ||
 			pg_hmac_final(ctx, ClientSignature, state->key_length) < 0)
 		{
-		elog(ERROR, "could not calculate client signature: %s",
-			 pg_hmac_error(ctx));
+			// TODO: Convert to DEBUG2
+			elog(LOG, "could not calculate client signature for secret %d", j);
+			pg_hmac_free(ctx);
+			continue;
 		}
 
+		// TODO: Convert to DEBUG2
+		elog(LOG, "succeeded on %d password", j);
+
 		pg_hmac_free(ctx);
 
 		/* Extract the ClientKey that the client calculated from the proof */
@@ -1180,10 +1221,15 @@ verify_client_proof(scram_state *state)
 					client_StoredKey, &errstr) < 0)
 			elog(ERROR, "could not hash stored key: %s", errstr);
 
-	if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0)
-		return false;
-
+		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;
 }
 
 /*
@@ -1409,7 +1455,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 81dabb9c27..3c18c8f616 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -57,8 +57,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);
 
 
 /*----------------------------------------------------------------
@@ -789,8 +788,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);
 
@@ -798,17 +798,21 @@ 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) {
+		for (i = 0; i < num_passwords; i++)
 		{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
+			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)
@@ -823,57 +827,51 @@ 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;
 
 	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);
-
-	/*
-	 * 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
-	 * 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)
-		pwtype = Password_encryption;
-	else
-		pwtype = get_password_type(shadow_pass);
+	/* First look up the user's passwords. */
+	passwords = get_role_passwords(port->user_name, logdetail, &num_passwords);
 
 	/*
 	 * 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 PW 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)
+	{
+		for (i = 0; i < num_passwords; i++)
+			if (get_password_type(passwords[i]) == PASSWORD_TYPE_SCRAM_SHA_256)
+			{
+				scram_pw_avail = true;
+				break;
+			}
+
+		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, shadow_pass,
+			auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num_passwords,
 											logdetail);
 
-	if (shadow_pass)
-		pfree(shadow_pass);
-
-	/*
-	 * If get_role_password() returned error, return error, even if the
-	 * authentication succeeded.
-	 */
-	if (!shadow_pass)
+		for (i = 0; i < num_passwords; i++)
 		{
-		Assert(auth_result != STATUS_OK);
-		return STATUS_ERROR;
+			if (passwords[i] != NULL)
+				pfree(passwords[i]);
+			else
+				ereport(DEBUG2,
+					(errmsg("Password %d was null", i)));
+		}
+		pfree(passwords);
 	}
 
 	if (auth_result == STATUS_OK)
@@ -883,11 +881,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))
@@ -903,12 +902,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 ef496a0bea..5880033f0c 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_auth_password.password.
  *
  * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -16,12 +16,14 @@
 #include <unistd.h>
 
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "common/md5.h"
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
 #include "miscadmin.h"
 #include "utils/builtins.h"
+#include "utils/catcache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
@@ -33,53 +35,108 @@
  * 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)
 {
-	TimestampTz vuntil = 0;
 	HeapTuple	roleTup;
 	Datum		datum;
+	TimestampTz current, vuntil = 0;
+
 	bool		isnull;
-	char	   *shadow_pass;
+	char	   **passwords;
+	CatCList   *passlist;
+	int		    i, j = 0, num_valid_passwords = 0;
+
+	*num = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
 	if (!HeapTupleIsValid(roleTup))
 	{
-		*logdetail = psprintf(_("Role \"%s\" does not exist."),
-							  role);
+		*logdetail = psprintf(_("Role \"%s\" does not exist."), role);
 		return NULL;			/* no such user */
 	}
 
 	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolpassword, &isnull);
-	if (isnull)
+							Anum_pg_authid_rolvaliduntil, &isnull);
+	if (!isnull)
+		vuntil = DatumGetTimestampTz(datum);
+
+	current = GetCurrentTimestamp();
+
+	/*
+	 * Check to be sure we are not past rolvaliduntil
+	 */
+	if (!isnull && vuntil < current)
 	{
+		/*
+		 * Although it is the role that has expired, not one of its passwords,
+		 * below we complain that the password has expired. This is to maintain
+		 * compatibility with external tools that may have come to depend on
+		 * this message when a role expires.
+		 */
+		*logdetail = psprintf(_("User \"%s\" has an expired password."),
+							  role);
+		return NULL;
+	}
+
+	datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull);
 	ReleaseSysCache(roleTup);
+
+	/* Find any existing password that is not the one being updated, to get the salt */
+	passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum);
+
+	if (passlist->n_members == 0)
+	{
 		*logdetail = psprintf(_("User \"%s\" has no password assigned."),
 							  role);
+		ReleaseCatCacheList(passlist);
 		return NULL;			/* user has no password */
 	}
-	shadow_pass = TextDatumGetCString(datum);
 
-	datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolvaliduntil, &isnull);
+	/*
+	 * TODO: Merge this and the following loop into one; allocate
+	 * valid_passwords array as long as n_members, then use repalloc() to shrink
+	 * the valid_passwords array.
+	 */
+	for (i = 0; i < passlist->n_members; i++)
+	{
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
 		if (!isnull)
 			vuntil = DatumGetTimestampTz(datum);
 
-	ReleaseSysCache(roleTup);
+		if (isnull || vuntil > current)
+			num_valid_passwords++;
+	}
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	passwords = palloc0(sizeof(char *) * num_valid_passwords);
+	*num = num_valid_passwords;
+
+	for (i = 0; i < passlist->n_members; i++)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		HeapTuple	tup = &passlist->members[i]->tuple;
+
+		datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+							Anum_pg_auth_password_expiration, &isnull);
+
+		if (!isnull)
+			vuntil = DatumGetTimestampTz(datum);
+
+		if (isnull || vuntil > current)
+		{
+			datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup,
+								Anum_pg_auth_password_password, &isnull);
+			passwords[j++] = pstrdup(TextDatumGetCString(datum));
 		}
+	}
+
+	ReleaseCatCacheList(passlist);
 
-	return shadow_pass;
+	return passwords;
 }
 
 /*
@@ -113,7 +170,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);
@@ -134,13 +191,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'");
@@ -158,7 +215,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_auth_password.password.
  * '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.
  *
@@ -213,7 +270,7 @@ 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_auth_password.password.
  * '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/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d2032885e..a8f48efcca 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -707,7 +707,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DOUBLE_P DROP
 
 	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPIRES EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -743,7 +743,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD
 	PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -1199,6 +1199,21 @@ AlterOptRoleElem:
 							 errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
 							 parser_errposition(@1)));
 				}
+			| PASSNAME Sconst
+				{
+					$$ = makeDefElem("passname",
+									 (Node *)makeString($2), @1);
+				}
+			| EXPIRES IN_P Sconst opt_interval
+				{
+					TypeName *t = SystemTypeName("interval");
+					t->typmods = $4;
+					$$ = makeDefElem("expiresin", (Node *)makeStringConstCast($3, @3, t), @1);
+				}
+			| VALID FOR Sconst
+				{
+					$$ = makeDefElem("validFor", (Node *)makeString($3), @1);
+				}
 			| INHERIT
 				{
 					$$ = makeDefElem("inherit", (Node *) makeBoolean(true), @1);
@@ -17110,6 +17125,7 @@ unreserved_keyword:
 			| EXCLUDING
 			| EXCLUSIVE
 			| EXECUTE
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -17214,6 +17230,7 @@ unreserved_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLANS
 			| POLICY
@@ -17670,6 +17687,7 @@ bare_label_keyword:
 			| EXCLUSIVE
 			| EXECUTE
 			| EXISTS
+			| EXPIRES
 			| EXPLAIN
 			| EXPRESSION
 			| EXTENSION
@@ -17815,6 +17833,7 @@ bare_label_keyword:
 			| PARTIAL
 			| PARTITION
 			| PASSING
+			| PASSNAME
 			| PASSWORD
 			| PLACING
 			| PLANS
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 1aacb736c2..71eb0b7f49 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1106,6 +1106,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey)
 
 		case AUTHNAME:
 		case AUTHOID:
+		case AUTHPASSWORDNAME:
 		case AUTHMEMMEMROLE:
 		case DATABASEOID:
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7234cb3da6..710470dd46 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_namespace.h"
@@ -114,6 +115,7 @@ static const FormData_pg_attribute Desc_pg_proc[Natts_pg_proc] = {Schema_pg_proc
 static const FormData_pg_attribute Desc_pg_type[Natts_pg_type] = {Schema_pg_type};
 static const FormData_pg_attribute Desc_pg_database[Natts_pg_database] = {Schema_pg_database};
 static const FormData_pg_attribute Desc_pg_authid[Natts_pg_authid] = {Schema_pg_authid};
+static const FormData_pg_attribute Desc_pg_auth_password[Natts_pg_auth_password] = {Schema_pg_auth_password};
 static const FormData_pg_attribute Desc_pg_auth_members[Natts_pg_auth_members] = {Schema_pg_auth_members};
 static const FormData_pg_attribute Desc_pg_index[Natts_pg_index] = {Schema_pg_index};
 static const FormData_pg_attribute Desc_pg_shseclabel[Natts_pg_shseclabel] = {Schema_pg_shseclabel};
@@ -3495,6 +3497,7 @@ RelationBuildLocalRelation(const char *relname,
 	{
 		case DatabaseRelationId:
 		case AuthIdRelationId:
+		case AuthPasswordRelationId:
 		case AuthMemRelationId:
 		case RelationRelationId:
 		case AttributeRelationId:
@@ -3970,7 +3973,7 @@ RelationCacheInitialize(void)
  *		RelationCacheInitializePhase2
  *
  *		This is called to prepare for access to shared catalogs during startup.
- *		We must at least set up nailed reldescs for pg_database, pg_authid,
+ *		We must at least set up nailed reldescs for pg_database, pg_authid, pg_auth_password,
  *		pg_auth_members, and pg_shseclabel. Ideally we'd like to have reldescs
  *		for their indexes, too.  We attempt to load this information from the
  *		shared relcache init file.  If that's missing or broken, just make
@@ -4011,12 +4014,14 @@ RelationCacheInitializePhase2(void)
 				  Natts_pg_authid, Desc_pg_authid);
 		formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true,
 				  Natts_pg_auth_members, Desc_pg_auth_members);
+		formrdesc("pg_auth_password", AuthPasswordRelation_Rowtype_Id, true,
+				  Natts_pg_auth_password, Desc_pg_auth_password);
 		formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true,
 				  Natts_pg_shseclabel, Desc_pg_shseclabel);
 		formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,
 				  Natts_pg_subscription, Desc_pg_subscription);
 
-#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_RELS	6	/* fix if you change list above */
 	}
 
 	MemoryContextSwitchTo(oldcxt);
@@ -4153,10 +4158,12 @@ RelationCacheInitializePhase3(void)
 							AuthIdRelationId);
 		load_critical_index(AuthMemMemRoleIndexId,
 							AuthMemRelationId);
+		load_critical_index(AuthPasswordRoleOidIndexId,
+							AuthPasswordRelationId);
 		load_critical_index(SharedSecLabelObjectIndexId,
 							SharedSecLabelRelationId);
 
-#define NUM_CRITICAL_SHARED_INDEXES 6	/* fix if you change list above */
+#define NUM_CRITICAL_SHARED_INDEXES 7	/* fix if you change list above */
 
 		criticalSharedRelcachesBuilt = true;
 	}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 8dbda0024f..f9caf4c1b8 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -27,6 +27,7 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_auth_members.h"
+#include "catalog/pg_auth_password.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_collation.h"
@@ -213,6 +214,13 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_authid_oid),
 		8
 	},
+	[AUTHPASSWORDNAME] = {
+		AuthPasswordRelationId,
+		AuthPasswordRoleOidIndexId,
+		KEY(Anum_pg_auth_password_roleid,
+			Anum_pg_auth_password_name),
+		8
+	},
 	[CASTSOURCETARGET] = {
 		CastRelationId,
 		CastSourceTargetIndexId,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index bdb26e2b77..4a5c3866d8 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -610,6 +610,8 @@ static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
 
+static char *password_duration_string;
+
 /* should be static, but commands/variable.c needs to get at this */
 char	   *role_string;
 
@@ -4540,6 +4542,16 @@ struct config_string ConfigureNamesString[] =
 		"",
 		check_debug_io_direct, assign_debug_io_direct, NULL
 	},
+	{
+		{"password_valid_duration", PGC_SUSET, CONN_AUTH_AUTH,
+			gettext_noop("Specifies the default validity duration of new passwords."),
+			NULL,
+			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
+		},
+		&password_duration_string,
+		NULL,
+		check_password_duration, assign_password_duration, show_password_duration
+	},
 
 	/* End-of-list marker */
 	{
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index bddb30d766..3c3b874580 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -1567,10 +1567,10 @@ static void
 setup_auth(FILE *cmdfd)
 {
 	/*
-	 * The authid table shouldn't be readable except through views, to ensure
+	 * The pg_auth_password table shouldn't be readable except through views, to ensure
 	 * passwords are not publicly visible.
 	 */
-	PG_CMD_PUTS("REVOKE ALL ON pg_authid FROM public;\n\n");
+	PG_CMD_PUTS("REVOKE ALL ON pg_auth_password FROM public;\n\n");
 
 	if (superuser_password)
 		PG_CMD_PRINTF("ALTER USER \"%s\" WITH PASSWORD E'%s';\n\n",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e2a9733d34..bfa15e9696 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -762,7 +762,18 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* note: rolconfig is dumped later */
-	if (server_version >= 90600)
+	if (server_version >= 170000)
+		printfPQExpBuffer(buf,
+						  "SELECT oid, rolname, rolsuper, rolinherit, "
+						  "rolcreaterole, rolcreatedb, "
+						  "rolcanlogin, rolconnlimit, p.password as rolpassword, "
+						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
+						  "rolname = current_user AS is_current_user "
+						  "FROM %s LEFT JOIN pg_auth_password p ON %s.oid = p.roleid "
+						  "WHERE rolname !~ '^pg_' "
+						  "ORDER BY 2", role_catalog, role_catalog, role_catalog);
+	else if (server_version >= 90600)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
diff --git a/src/common/scram-common.c b/src/common/scram-common.c
index ef997ef684..df48f74f88 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -185,7 +185,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_auth_password.password.
  *
  * The password should already have been processed with SASLprep, if necessary!
  *
diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h
new file mode 100644
index 0000000000..a1e0741d63
--- /dev/null
+++ b/src/include/catalog/pg_auth_password.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_auth_password.h
+ *	  definition of the "authorization identifier" system catalog (pg_auth_password)
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_auth_password.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_AUTH_PASSWORD_H
+#define PG_AUTH_PASSWORD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_auth_password_d.h"
+
+/* ----------------
+ *		pg_auth_password definition.  cpp turns this into
+ *		typedef struct FormData_pg_auth_password
+ * ----------------
+ */
+CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO
+{
+	Oid			roleid BKI_LOOKUP(pg_authid);	/* ID of a role */
+	NameData    name;            /* name of password for multiple password support */
+
+#ifdef CATALOG_VARLEN                /* variable-length fields start here */
+	text            password BKI_FORCE_NOT_NULL;        /* password */
+	timestamptz     expiration BKI_FORCE_NULL;	        /* password expiration time, if any */
+#endif
+} FormData_pg_auth_password;
+
+/* ----------------
+ *		Form_pg_auth_password corresponds to a pointer to a tuple with
+ *		the format of pg_auth_password relation.
+ * ----------------
+ */
+
+typedef FormData_pg_auth_password *Form_pg_auth_password;
+
+DECLARE_TOAST_WITH_MACRO(pg_auth_password, 9122, 9133, PgAuthPasswordToastTable, PgAuthPasswordToastIndex);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 9134, AuthPasswordRoleOidIndexId, pg_auth_password, btree(roleid oid_ops, name name_ops));
+
+#endif							/* PG_AUTH_PASSWORD_H */
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6b4a0aaaad..1dbf5ca0e0 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,76 +23,76 @@
   rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
   rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
   rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
-  rolpassword => '_null_', rolvaliduntil => '_null_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_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_' },
+  rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 0e7ddc56ea..56190512cf 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -42,9 +42,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	int32		rolconnlimit;	/* max connections allowed (-1=no limit) */
 
 	/* remaining fields may be null; use heap_getattr to read them! */
-#ifdef CATALOG_VARLEN			/* variable-length fields start here */
-	text		rolpassword;	/* password, if any */
-	timestamptz rolvaliduntil;	/* password expiration time, if any */
+#ifdef CATALOG_VARLEN
+	timestamptz rolvaliduntil BKI_FORCE_NULL;	/* role expiration time, if any */
 #endif
 } FormData_pg_authid;
 
@@ -55,8 +54,6 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
  */
 typedef FormData_pg_authid *Form_pg_authid;
 
-DECLARE_TOAST_WITH_MACRO(pg_authid, 4175, 4176, PgAuthidToastTable, PgAuthidToastIndex);
-
 DECLARE_UNIQUE_INDEX(pg_authid_rolname_index, 2676, AuthIdRolnameIndexId, pg_authid, btree(rolname name_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_authid_oid_index, 2677, AuthIdOidIndexId, pg_authid, btree(oid oid_ops));
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 97dcb93791..75bb58eea8 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -21,6 +21,9 @@
 extern PGDLLIMPORT int Password_encryption; /* values from enum PasswordType */
 extern PGDLLIMPORT char *createrole_self_grant;
 
+/* GUC. system-wide password validity duration */
+extern Interval *default_password_duration;
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index ddcd27469a..2b66fd909a 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -21,7 +21,7 @@
  * 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
+ * in pg_auth_password.password. 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 7a1b1ed0a0..12c5c9602b 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 310bc36517..07d9cc3990 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,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..af0bba6244 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -160,6 +160,7 @@ PG_KEYWORD("excluding", EXCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exclusive", EXCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("execute", EXECUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("exists", EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("expires", EXPIRES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("explain", EXPLAIN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("expression", EXPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("extension", EXTENSION, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -325,6 +326,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index f04b99e3b9..554c3c06b3 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -161,4 +161,8 @@ extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern bool check_wal_segment_size(int *newval, void **extra, GucSource source);
 extern void assign_xlog_sync_method(int new_sync_method, void *extra);
 
+extern bool check_password_duration(char **newval, void **extra, GucSource source);
+extern void assign_password_duration(const char *newval, void *extra);
+extern const char *show_password_duration(void);
+
 #endif							/* GUC_HOOKS_H */
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 67ea6e4945..6015f53d73 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -43,6 +43,7 @@ enum SysCacheIdentifier
 	AUTHMEMROLEMEM,
 	AUTHNAME,
 	AUTHOID,
+	AUTHPASSWORDNAME,
 	CASTSOURCETARGET,
 	CLAAMNAMENSP,
 	CLAOID,
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index acfd9d1f4f..3ba23628a1 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -2521,10 +2521,10 @@ REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
 ERROR:  cannot reindex system catalogs concurrently
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 ERROR:  cannot reindex system catalogs concurrently
@@ -2830,10 +2830,10 @@ ERROR:  must be owner of schema schema_to_reindex
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-ERROR:  must be owner of table pg_toast_1260
-REINDEX INDEX pg_toast.pg_toast_1260_index;
-ERROR:  must be owner of index pg_toast_1260_index
+REINDEX TABLE pg_toast.pg_toast_1262;
+ERROR:  must be owner of table pg_toast_1262
+REINDEX INDEX pg_toast.pg_toast_1262_index;
+ERROR:  must be owner of index pg_toast_1262_index
 -- Clean up
 RESET ROLE;
 REVOKE USAGE ON SCHEMA pg_toast FROM regress_reindexuser;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..b69a2a72a7 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,6 +197,7 @@ NOTICE:  checking pg_tablespace {spcowner} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {roleid} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {member} => pg_authid {oid}
 NOTICE:  checking pg_auth_members {grantor} => pg_authid {oid}
+NOTICE:  checking pg_auth_password {roleid} => pg_authid {oid}
 NOTICE:  checking pg_shdepend {dbid} => pg_database {oid}
 NOTICE:  checking pg_shdepend {classid} => pg_class {oid}
 NOTICE:  checking pg_shdepend {refclassid} => pg_class {oid}
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 8475231735..0d44ac3915 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -24,10 +24,12 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5783277baca28003b33453252be4dbb34
@@ -40,12 +42,14 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 NOTICE:  MD5 password cleared because of role rename
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
-       rolname       | rolpassword 
----------------------+-------------
+    ORDER BY rolname, password;
+       rolname       | password 
+---------------------+----------
  regress_passwd2_new | 
 (1 row)
 
@@ -75,10 +79,12 @@ CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 -- Changing the SCRAM iteration count
 SET scram_iterations = 1024;
 CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount';
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
      rolname     |                rolpassword_masked                 
 -----------------+---------------------------------------------------
  regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70
@@ -99,9 +105,11 @@ ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 NOTICE:  empty string is not a valid password, clearing password
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
 NOTICE:  empty string is not a valid password, clearing password
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
- rolpassword 
--------------
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
+ password 
+----------
  
 (1 row)
 
@@ -114,8 +122,10 @@ CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
          rolname         | is_rolpassword_rehashed 
@@ -139,11 +149,13 @@ DROP ROLE regress_passwd_sha_len0;
 DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
- rolname | rolpassword 
----------+-------------
+    ORDER BY rolname, password;
+ rolname | password 
+---------+----------
 (0 rows)
 
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173..18a9dacadd 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -1,233 +1,233 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_superuser | t        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
-       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+       rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_inherit | f        | f          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
-           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+           rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
-         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+         rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createrole | f        | t          | t             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
-          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
-        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+        rolname        | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+-----------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_createdb | f        | t          | f             | t           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_role_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
-            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+            rolname             | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | t           | f              | f            |           -1 | 
 (1 row)
 
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_user_canlogin | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
-           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+           rolname            | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
-         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+         rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+--------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_replication | f        | t          | f             | f           | f           | t              | f            |           -1 | 
 (1 row)
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
-          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+          rolname           | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+----------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_def_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | f            |           -1 | 
 (1 row)
 
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
-        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvaliduntil 
-------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+---------------
- regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 |             | 
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+        rolname         | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolvaliduntil 
+------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+---------------
+ regress_test_bypassrls | f        | t          | f             | f           | f           | f              | t            |           -1 | 
 (1 row)
 
 -- clean up roles
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5058be5411..ddbf56b940 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1727,11 +1727,12 @@ pg_shadow| SELECT pg_authid.rolname AS usename,
     pg_authid.rolsuper AS usesuper,
     pg_authid.rolreplication AS userepl,
     pg_authid.rolbypassrls AS usebypassrls,
-    pg_authid.rolpassword AS passwd,
+    p.password AS passwd,
     pg_authid.rolvaliduntil AS valuntil,
     s.setconfig AS useconfig
-   FROM (pg_authid
+   FROM ((pg_authid
      LEFT JOIN pg_db_role_setting s ON (((pg_authid.oid = s.setrole) AND (s.setdatabase = (0)::oid))))
+     LEFT JOIN pg_auth_password p ON (p.roleid = pg_authid.oid))
   WHERE pg_authid.rolcanlogin;
 pg_shmem_allocations| SELECT name,
     off,
diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out
index 9aabb85349..dd535d41a3 100644
--- a/src/test/regress/expected/tablespace.out
+++ b/src/test/regress/expected/tablespace.out
@@ -51,13 +51,13 @@ ERROR:  cannot move system relation "pg_authid_rolname_index"
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 ERROR:  cannot reindex system catalogs concurrently
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
 ERROR:  cannot reindex system catalogs concurrently
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-ERROR:  cannot move system relation "pg_toast_1260_index"
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+ERROR:  cannot move system relation "pg_toast_1262_index"
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 ERROR:  cannot reindex system catalogs concurrently
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql
index d49ce9f300..d2c63e424c 100644
--- a/src/test/regress/sql/create_index.sql
+++ b/src/test/regress/sql/create_index.sql
@@ -1074,9 +1074,9 @@ REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 COMMIT;
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
--- These are the toast table and index of pg_authid.
-REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1260; -- no catalog toast table
-REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1260_index; -- no catalog toast index
+-- These are the toast table and index of pg_database.
+REINDEX TABLE CONCURRENTLY pg_toast.pg_toast_1262; -- no catalog toast table
+REINDEX INDEX CONCURRENTLY pg_toast.pg_toast_1262_index; -- no catalog toast index
 REINDEX SYSTEM CONCURRENTLY postgres; -- not allowed for SYSTEM
 REINDEX (CONCURRENTLY) SYSTEM postgres; -- ditto
 REINDEX (CONCURRENTLY) SYSTEM;  -- ditto
@@ -1253,8 +1253,8 @@ REINDEX SCHEMA schema_to_reindex;
 RESET ROLE;
 GRANT USAGE ON SCHEMA pg_toast TO regress_reindexuser;
 SET SESSION ROLE regress_reindexuser;
-REINDEX TABLE pg_toast.pg_toast_1260;
-REINDEX INDEX pg_toast.pg_toast_1260_index;
+REINDEX TABLE pg_toast.pg_toast_1262;
+REINDEX INDEX pg_toast.pg_toast_1262_index;
 
 -- Clean up
 RESET ROLE;
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index 53e86b0b6c..b4b6ab9847 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -23,18 +23,22 @@ CREATE ROLE regress_passwd4 PASSWORD NULL;
 --
 -- Since the salt is random, the exact value stored will be different on every test
 -- run. Use a regular expression to mask the changing parts.
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- Rename a role
 ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new;
 -- md5 entry should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd2_new'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 
 -- Change passwords with ALTER USER. With plaintext or already-encrypted
@@ -67,16 +71,20 @@ CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 SET scram_iterations = 1024;
 CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount';
 
-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
+SELECT rolname, regexp_replace(password, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
 ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a';
 ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4=';
-SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
+SELECT password FROM pg_authid
+LEFT JOIN pg_auth_password p
+ON pg_authid.oid = p.roleid WHERE rolname='regress_passwd_empty';
 
 -- Test with invalid stored and server keys.
 --
@@ -88,8 +96,10 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941
 
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
-SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
+SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd_sha_len%'
     ORDER BY rolname;
 
@@ -108,7 +118,9 @@ DROP ROLE regress_passwd_sha_len1;
 DROP ROLE regress_passwd_sha_len2;
 
 -- all entries should have been removed
-SELECT rolname, rolpassword
+SELECT rolname, password
     FROM pg_authid
+    LEFT JOIN pg_auth_password p
+    ON pg_authid.oid = p.roleid
     WHERE rolname LIKE 'regress_passwd%'
-    ORDER BY rolname, rolpassword;
+    ORDER BY rolname, password;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d730..09505c6a3b 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -1,83 +1,83 @@
 -- default for superuser is false
 CREATE ROLE regress_test_def_superuser;
 
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_superuser';
 CREATE ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH NOSUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 ALTER ROLE regress_test_superuser WITH SUPERUSER;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_superuser';
 
 -- default for inherit is true
 CREATE ROLE regress_test_def_inherit;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_inherit';
 CREATE ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH INHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 ALTER ROLE regress_test_inherit WITH NOINHERIT;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_inherit';
 
 -- default for create role is false
 CREATE ROLE regress_test_def_createrole;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createrole';
 CREATE ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH NOCREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 ALTER ROLE regress_test_createrole WITH CREATEROLE;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createrole';
 
 -- default for create database is false
 CREATE ROLE regress_test_def_createdb;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_createdb';
 CREATE ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH NOCREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 ALTER ROLE regress_test_createdb WITH CREATEDB;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_createdb';
 
 -- default for can login is false for role
 CREATE ROLE regress_test_def_role_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_role_canlogin';
 CREATE ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 ALTER ROLE regress_test_role_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_role_canlogin';
 
 -- default for can login is true for user
 CREATE USER regress_test_def_user_canlogin;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_user_canlogin';
 CREATE USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH LOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 ALTER USER regress_test_user_canlogin WITH NOLOGIN;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_user_canlogin';
 
 -- default for replication is false
 CREATE ROLE regress_test_def_replication;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_replication';
 CREATE ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH NOREPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 ALTER ROLE regress_test_replication WITH REPLICATION;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_replication';
 
 -- default for bypassrls is false
 CREATE ROLE regress_test_def_bypassrls;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_def_bypassrls';
 CREATE ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH NOBYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 ALTER ROLE regress_test_bypassrls WITH BYPASSRLS;
-SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolpassword, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
+SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolbypassrls, rolconnlimit, rolvaliduntil FROM pg_authid WHERE rolname = 'regress_test_bypassrls';
 
 -- clean up roles
 DROP ROLE regress_test_def_superuser;
diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql
index d274d9615e..c8b83788f0 100644
--- a/src/test/regress/sql/tablespace.sql
+++ b/src/test/regress/sql/tablespace.sql
@@ -40,10 +40,10 @@ REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_am;
 REINDEX (TABLESPACE regress_tblspace) TABLE pg_authid;
 REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_authid;
 -- toast relations, fail
-REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1260_index;
-REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1260;
-REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1260;
+REINDEX (TABLESPACE regress_tblspace) INDEX pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) INDEX CONCURRENTLY pg_toast.pg_toast_1262_index;
+REINDEX (TABLESPACE regress_tblspace) TABLE pg_toast.pg_toast_1262;
+REINDEX (TABLESPACE regress_tblspace) TABLE CONCURRENTLY pg_toast.pg_toast_1262;
 -- system catalog, fail
 REINDEX (TABLESPACE pg_global) TABLE pg_authid;
 REINDEX (TABLESPACE pg_global) TABLE CONCURRENTLY pg_authid;
#12Jeff Davis
pgsql@j-davis.com
In reply to: Gurjeet Singh (#11)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Mon, 2023-09-25 at 00:31 -0700, Gurjeet Singh wrote:

Please see attached v4 of the patch. The patch takes care of rebase
to
the master/17-devel branch, and includes some changes, too.

FWIW I got some failures applying. I didn't investigate much, and
instead I looked at your git branch (7a35619e).

Moreover, before the patch, in case of CheckPasswordAuth(), the error
(if any) would have been thrown _after_ network communication done by
sendAuthRequest() call. But with this patch, the error is thrown
before the network interaction, hence this changes the order of
network interaction and the error message. This may have security
implications, too, but I'm unable to articulate one right now.

You mean before v3 or before v4? Is this currently a problem in v4?

Open question: If a client is capable of providing just md5 passwords
handshake, and because of pg_hba.conf setting, or because the role
has
at least one SCRAM password (essentially the 3rd case you mention
above: pg_hba md5 + md5 and scram pws -> scram), the server will
respond with a SASL/SCRAM authentication response, and that would
break the backwards compatibility and will deny access to the client.
Does this make it necessary to use a newer libpq/client library?

Perhaps you can try the MD5 passwords first, and only if they fail,
move on to try scram passwords?

Comments?

IIUC, for the case of multiple scram passwords, we use the salt to
select the right scram password, and then proceed from there?

I'm not very excited about the idea of naming passwords, or having
passwords with default names. I can't think of anything better right
now, so it might be OK.

- Add tests
- Add/update documentation

These are needed to provide better review.

Regards,
Jeff Davis

#13Gurjeet Singh
gurjeet@singh.im
In reply to: Jeff Davis (#12)
Re: [PoC/RFC] Multiple passwords, interval expirations

I had an idea to simplify this feature/patch and after some validation
in internal discussions, I am posting the new approach here. I'd
appreciate any feedback and comments.

To begin with, the feature we are chasing is to make it convenient for
the users to rollover their passwords. Currently there is no easy way
to rollover passwords without increasing the risk of an application
outage. After a password change, the users/admins have to rush to
change the password in all locations where it is stored. There is a
window of time where if the application password is not changed to the
new one, and the application tries to connect/reconnect for any
reason, the application will fail authentication and lead to an outage.

I personally haven't seen any attempts by any
application/driver/framework to solve this problem in the wild, so
following is just me theorizing how one may solve this problem on the
application side; there may be other ways in which others may solve
this problem. The application may be written in such a way that upon
password authentication failure, it tries again with a second
password. The application's configuration file (or environment
variables) may allow specifying 2 passwords at the same time, and the
application will keep trying these 2 passwords alternatively until it
succeeds or the user restarts it with a new configuration. With such a
logic in place in their application, the users may first change the
configuration of all the instances of the application to hold the new
password along with the old/current working password, and only then
change the password in the database. This way, in the event of an
application instance start/restart either the old password will
succeed, or the new password will.

There may be other ways to solve this problem, but I can't imagine any
of those ways to be convenient and straightforward. At least not as
convenient as it can be if the database itself allowed for storing
both the passwords, and honored both passwords at the same time, while
allowing to associate a separate validity period with each of the
passwords.

The patches posted in this thread so far attempt to add the ability to
allow the user to have an arbitrary number of passwords. I believe
that allowing arbitrary number of passwords is not only unnecessary,
but the need to name passwords, the need to store them in a shared
catalog, etc. may actually create problems in the field. The
users/admins will have to choose names for passwords, which they
didn't have to previously. The need to name them may also lead to
users storing password-hints in the password names (e.g. 'mom''s
birthday', 'ex''s phone number', 'third password'), rendering the
passwords weak.

Moreover, allowing an arbitrarily many number of passwords will
require us to provide additional infrastructure to solve problems like
observability (which passwords are currently in use, and which ones
have been effectively forgotten by applications), or create a nuisance
for admins that can create more problems than it solves.

So I propose that the feature should allow no more than 2 passwords
for a role, each with their own validity periods. This eliminates the
need to name passwords, because at any given time there are no more
than 2 passwords; current one, and old one. This also eliminates the
need for a shared catalog to hold passwords, because with the limit of
2 imposed, we can store the old password and its validity period in
additional columns in the pg_authid table.

The patches so far also add a notion of max validity period of
passwords, which only a superuser can override. I believe this is a
useful feature, but that feature can be dealt with separately,
independent of password rollover feature. So in the newer patches I
will not include the relevant GUC and code.

With the above being said, following is the user interface I can think
of that can allow for various operations that users may need to
perform to rollover their passwords. The 'ADD PASSWORD' and 'ALL
PASSWORD' are additions to the grammar. rololdpassword and
rololdvaliduntil will be new columns in pg_authid that will hold the
old password and its valid-until value.

In essence, we create a stack that can hold 2 passwords. Pushing an
element when it's full will make it forget the bottom element. Popping
the stack makes it forget the top element, and the only remaining
element, if any, becomes the top.

-- Create a user, as usual
CREATE ROLE u1 PASSWORD 'p1' VALID UNTIL '2020/01/01';

-- Add another password that the user can use for authentication. This moves
-- the 'p1' password hash and its valid-until value to rololdpassword and
-- rololdvaliduntil, respectively.
ALTER ROLE u1 ADD PASSWORD 'p2' VALID UNTIL '2021/01/01';

-- Change the password 'p2's (current password's) validity
ALTER ROLE u1 VALID UNTIL '2022/01/01';
-- Note that currently I don't have a proposal for how to change the old
-- password's validity period, without deleting the latest/main password. See
-- PASSWORD NULL command below on how to delete the current password. It's very
-- likely that in a password rollover use case it's unnecessary, even
-- undesirable, to change the old password's validity period.

-- If, for some reason, the user wants to get rid of the latest password added.
-- Remove 'p2' (current password). The old password (p1), will be restored to
-- rolpassword, along with its valid-until value.
ALTER ROLE u1 PASSWORD NULL;
-- This may come as a surprise to some users, because currently they expect the
-- user to completely lose the ability to use passwords for login after this
-- command. To get the old behavior, the user must now use the ALL PASSWORD
-- NULL incantation; see below.
-- Issuing this command one more time will remove even the restored password,
-- hence leaving the user with no passwords.

-- Change the validity of the restored/current password (p1)
ALTER ROLE u1 VALID UNTIL '2022/01/01';

-- Add a new password (p3) without affecting old password's (p1) validity
ALTER ROLE u1 ADD PASSWORD 'p3' VALID UNTIL '2023/01/01';

-- Add a new password 'p4'. This will move 'p3' to rololdpassword, and hence
-- 'p1' will be forgotten completely.
-- After this command, user can use passwords 'p3' (old) and 'p4' (current) to
-- login.
ALTER ROLE u1 ADD PASSWORD 'p4' VALID UNTIL '2024/01/01';

-- Replace 'p4' (current) password with 'p5'. Note that this command is _not_
-- using the ADD keyword, hence 'p4' is _not_ moved to rololdpassword column.
-- After this command, user can use passwords 'p3' (old) and 'p5'
-- (current) to login.
ALTER ROLE u1 PASSWORD 'p5' VALID UNTIL '2025/01/01';

-- Using the old password to login will produce a warning, hopefully nudging
-- the user to start using the new password.
export PGPASSWORD=p3
psql -U u1
...
WARNING: Used old password to login

export PGPASSWORD=p5
psql -U u1
...
=> (no warning)

-- Remove all passwords from the role. Even old password, if any, is removed.
ALTER ROLE u1 ALL PASSWORD NULL;

In normal use, the users can simply keep ADDing new passwords, and the
database will promptly remember only one old password, and keep
forgetting any passwords older than that. But on the off chance
that someone needs to forget the latest password they added, and
restore the old password to be the "current" password, they can use
the PASSWORD NULL incantation. Note that this will result in
rol*old*password being set to NULL, because our password memory stack
cannot hold more than 2 elements.

Since this feature is targeted towards password rollovers, it's a legitimate
question to ask if we should enforce that the new password being added has a
valid-until greater than the valid-until of the existing/old password. I don't
think we should enforce this, at least not in this patch, because the
user/admin may have a use case where they want a short-lived new password that
they intend/want to change very soon; I'm thinking of cases where passwords are
being rolled over while they are also moving from older clients/libraries that
don't yet support scram-sha-256; keep using md5 and add passwords to honor
password rollover policy, but then as soon as all clients have been updated and
have the ability to use scram-sha-256, rollover the password again to utilize
the better mechanism.

I realize that allowing for a maximum of 2 passwords goes against the
zero-one-infinity rule [1]https://en.wikipedia.org/wiki/Zero_one_infinity_rule, but I think in the case of password
rollovers it's perfectly acceptable to limit the number of active
passwords to just 2. If there are use cases, either related to password
rollovers, or in its vicinity, that can be better addressed by
allowing an arbitrarily many passwords, I would love to learn about
them and change this design to accommodate for those use cases, or
perhaps revert to pursuing the multiple-passwords feature.

[1]: https://en.wikipedia.org/wiki/Zero_one_infinity_rule

Best regards,
Gurjeet
http://Gurje.et

#14Nathan Bossart
nathandbossart@gmail.com
In reply to: Gurjeet Singh (#13)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Wed, Oct 04, 2023 at 10:41:15PM -0700, Gurjeet Singh wrote:

The patches posted in this thread so far attempt to add the ability to
allow the user to have an arbitrary number of passwords. I believe
that allowing arbitrary number of passwords is not only unnecessary,
but the need to name passwords, the need to store them in a shared
catalog, etc. may actually create problems in the field. The
users/admins will have to choose names for passwords, which they
didn't have to previously. The need to name them may also lead to
users storing password-hints in the password names (e.g. 'mom''s
birthday', 'ex''s phone number', 'third password'), rendering the
passwords weak.

Moreover, allowing an arbitrarily many number of passwords will
require us to provide additional infrastructure to solve problems like
observability (which passwords are currently in use, and which ones
have been effectively forgotten by applications), or create a nuisance
for admins that can create more problems than it solves.

IMHO neither of these problems seems insurmountable. Besides advising
against using hints as names, we could also automatically generate safe
names, or even disallow user-provided names entirely. And adding
observability for passwords seems worthwhile anyway.

So I propose that the feature should allow no more than 2 passwords
for a role, each with their own validity periods. This eliminates the
need to name passwords, because at any given time there are no more
than 2 passwords; current one, and old one. This also eliminates the
need for a shared catalog to hold passwords, because with the limit of
2 imposed, we can store the old password and its validity period in
additional columns in the pg_authid table.

Another approach could be to allow an abritrary number of passwords but to
also allow administrators to limit how many passwords can be associated to
each role. That way, we needn't restrict this feature to 2 passwords for
everyone. Perhaps 2 should be the default, but in any case, IMO we
shouldn't design to only support 2.

In essence, we create a stack that can hold 2 passwords. Pushing an
element when it's full will make it forget the bottom element. Popping
the stack makes it forget the top element, and the only remaining
element, if any, becomes the top.

I think this would be mighty confusing to users since it's not clear that
adding a password will potentially invalidate a current password (which
might be actively in use), but only if there are already 2 in the stack. I
worry that such a desіgn might be too closely tailored to the
implementation details. If we proceed with this design, perhaps we should
consider ERROR-ing if a user tries to add a third password.

-- If, for some reason, the user wants to get rid of the latest password added.
-- Remove 'p2' (current password). The old password (p1), will be restored to
-- rolpassword, along with its valid-until value.
ALTER ROLE u1 PASSWORD NULL;
-- This may come as a surprise to some users, because currently they expect the
-- user to completely lose the ability to use passwords for login after this
-- command. To get the old behavior, the user must now use the ALL PASSWORD
-- NULL incantation; see below.
-- Issuing this command one more time will remove even the restored password,
-- hence leaving the user with no passwords.

Is it possible to remove the oldest password added without removing the
latest password added?

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#15Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#14)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Thu, 2023-10-05 at 14:04 -0500, Nathan Bossart wrote:

IMHO neither of these problems seems insurmountable.  Besides
advising
against using hints as names, we could also automatically generate
safe
names, or even disallow user-provided names entirely.  

I'd like to see what this looks like as a user-interface. Using a name
seems weird because of the reasons Gurjeet mentioned.

Using a number seems weird to me because either:

(a) if the number is always increasing you'd have to look to find the
number of the new slot to add and the old slot to remove; or
(b) if switched between two numbers (say 0 and 1), it would be error
prone because you'd have to remember which is the old one that can be
safely replaced

Maybe a password is best described by its validity period rather than a
name? But what about passwords that don't expire?

And adding
observability for passwords seems worthwhile anyway.

That might be useful just to know whether a user's password is even
being used -- in case the admin makes a mistake and some other auth
method is being used. Also it would help to know when a password can
safely be removed.

  That way, we needn't restrict this feature to 2 passwords for
everyone.  Perhaps 2 should be the default, but in any case, IMO we
shouldn't design to only support 2.

Are there use cases for lots of passwords, or is it just a matter of
not introducing an artificial limitation?

Would it ever make sense to have a role that has two permanent
passwords, or would that be an abuse of this feature? Any use cases I
can think of would be better solved with multiple user that are part of
the same group.

I think this would be mighty confusing to users since it's not clear
that
adding a password will potentially invalidate a current password
(which
might be actively in use), but only if there are already 2 in the
stack.  I
worry that such a desіgn might be too closely tailored to the
implementation details.  If we proceed with this design, perhaps we
should
consider ERROR-ing if a user tries to add a third password.

I agree that the proposed language is confusing, especially because ADD
causes a password to be added and another one to be removed. But
perhaps there are some non-confusing ways to expose a similar idea.

The thing I like about Gurjeet's proposal is that it's well-targeted at
a specific use case rather than trying to be too general. That makes it
a little easier to avoid certain problems like having a process that
adds passwords and never removes the old ones (leading to weird
problems like 47000 passwords for one user).

But it also feels strange to be limited to two -- perhaps the password
rotation schedule or policy just doesn't work with a limit of two, or
perhaps that introduces new kinds of mistakes.

Another idea: what if we introduce the notion of deprecating a
password? To remove a password, it would have to be deprecated first,
and maybe that would cause a LOG or WARNING message to be emitted when
used, or show up differently in some system view. And perhaps you could
have at most one non-deprecated password. That would give a workflow
something like (I'm not proposing these exact keywords):

CREATE USER foo PASSWORD 'secret1';
ALTER USER foo DEPRECATE PASSWORD; -- 'secret1' still works
ALTER USER foo PASSWORD 'secret2'; -- 'secret1' or 'secret2' works
... fix some applications
SET log_deprecated_password_use = WARNING;

...
WARNING: deprecated password used for user 'foo'
... fix some applications you forgot about
... warnings quiet down
ALTER USER foo DROP DEPRECATED PASSWORD; -- only 'secret2' works

If the user wants to un-deprecate a password (before they drop it, of
course), they can do something like:

ALTER USER foo PASSWORD NULL; -- 'secret2' removed
ALTER USER foo UNDEPRECATE PASSWORD; -- 'secret1' restored

if we allow multiple deprecated passwords, we'd still have to come up
with some way to address them (names, numbers, validity period,
something). But by isolating the problem to deprecated passwords only,
it feels like the system is still being restored to a clean state with
at most one single current password. The awkwardness is contained to
old passwords which will hopefully go away soon anyway and not
represent permanent clutter.

Regards,
Jeff Davis

#16Gurjeet Singh
gurjeet@singh.im
In reply to: Nathan Bossart (#14)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Thu, Oct 5, 2023 at 12:04 PM Nathan Bossart <nathandbossart@gmail.com> wrote:

On Wed, Oct 04, 2023 at 10:41:15PM -0700, Gurjeet Singh wrote:

The patches posted in this thread so far attempt to add the ability to
allow the user to have an arbitrary number of passwords. I believe
that allowing arbitrary number of passwords is not only unnecessary,
but the need to name passwords, the need to store them in a shared
catalog, etc. may actually create problems in the field. The
users/admins will have to choose names for passwords, which they
didn't have to previously. The need to name them may also lead to
users storing password-hints in the password names (e.g. 'mom''s
birthday', 'ex''s phone number', 'third password'), rendering the
passwords weak.

Moreover, allowing an arbitrarily many number of passwords will
require us to provide additional infrastructure to solve problems like
observability (which passwords are currently in use, and which ones
have been effectively forgotten by applications), or create a nuisance
for admins that can create more problems than it solves.

IMHO neither of these problems seems insurmountable.

Agreed.

Besides advising
against using hints as names, we could also automatically generate safe
names, or even disallow user-provided names entirely.

Somehow naming passwords doesn't feel palatable to me.

And adding
observability for passwords seems worthwhile anyway.

Agreed.

So I propose that the feature should allow no more than 2 passwords
for a role, each with their own validity periods. This eliminates the
need to name passwords, because at any given time there are no more
than 2 passwords; current one, and old one. This also eliminates the
need for a shared catalog to hold passwords, because with the limit of
2 imposed, we can store the old password and its validity period in
additional columns in the pg_authid table.

Another approach could be to allow an abritrary number of passwords but to
also allow administrators to limit how many passwords can be associated to
each role. That way, we needn't restrict this feature to 2 passwords for
everyone. Perhaps 2 should be the default, but in any case, IMO we
shouldn't design to only support 2.

I don't see a real use case to support more than 2 passwords. Allowing
an arbitrary number of passwords might look good and clean from
aesthetics and documentation perspective (no artificially enforced
limits, as in the zero-one-infinity rule), but in absence of real use
cases for that many passwords, I'm afraid we might end up with a
feature that creates more and worse problems for the users than it
solves.

In essence, we create a stack that can hold 2 passwords. Pushing an
element when it's full will make it forget the bottom element. Popping
the stack makes it forget the top element, and the only remaining
element, if any, becomes the top.

I think this would be mighty confusing to users since it's not clear that
adding a password will potentially invalidate a current password (which
might be actively in use), but only if there are already 2 in the stack.

Fair point. We can aid the user by emitting a NOTICE (or a WARNING)
message whenever an old password is removed from the system because of
addition of a new password.

I
worry that such a desіgn might be too closely tailored to the
implementation details. If we proceed with this design, perhaps we should
consider ERROR-ing if a user tries to add a third password.

I did not have a stack in mind when developing the use case and the
grammar, so implementation details did not drive this design. This new
design was more of a response to the manageability nightmare that I
could see the old approach may lead to. When writing the email I
thought mentioning the stack analogy may make it easier to develop a
mental model. I certainly won't suggest using it in the docs for
explanation :-)

-- If, for some reason, the user wants to get rid of the latest password added.
-- Remove 'p2' (current password). The old password (p1), will be restored to
-- rolpassword, along with its valid-until value.
ALTER ROLE u1 PASSWORD NULL;
-- This may come as a surprise to some users, because currently they expect the
-- user to completely lose the ability to use passwords for login after this
-- command. To get the old behavior, the user must now use the ALL PASSWORD
-- NULL incantation; see below.
-- Issuing this command one more time will remove even the restored password,
-- hence leaving the user with no passwords.

Is it possible to remove the oldest password added without removing the
latest password added?

In the patch I have so far, ALTER ROLE u1 ADD PASSWORD '' (empty
string) will drop the old password (what you asked for), and move the
current password to rololdpassword (which is not exactly what you
asked for :-). Hence the oldest password will be forgotten, and the
current password will continue to work;

Perhaps an explicit syntax like ALTER ROLE u1 DROP OLD PASSWORD can be
used for this.

Best regards,
Gurjeet
http://Gurje.et

#17Gurjeet Singh
gurjeet@singh.im
In reply to: Jeff Davis (#15)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Thu, Oct 5, 2023 at 1:09 PM Jeff Davis <pgsql@j-davis.com> wrote:

I think this would be mighty confusing to users since it's not clear
that
adding a password will potentially invalidate a current password
(which
might be actively in use), but only if there are already 2 in the
stack. I
worry that such a desіgn might be too closely tailored to the
implementation details. If we proceed with this design, perhaps we
should
consider ERROR-ing if a user tries to add a third password.

I agree that the proposed language is confusing, especially because ADD
causes a password to be added and another one to be removed. But
perhaps there are some non-confusing ways to expose a similar idea.

How about a language like the following (I haven't tried if this will
work in the grammar we have):

CREATE ROLE u1 PASSWORD 'p1';

ALTER ROLE u1ADD NEW PASSWORD 'p2';

ALTER ROLE u1 ADD NEW PASSOWRD 'p3';
ERROR: Cannot have more than 2 passwords at the same time.

ALTER ROLE u1 DROP OLD PASSWORD;

ALTER ROLE u1 ADD NEW PASSOWRD 'p3';
-- succeeds; forgets password 'p1'; p2 and p3 can be used to login

ALTER ROLE u1 DROP NEW PASSWORD;
-- forgets password 'p3'. Only 'p2' can be used to login

ALTER ROLE u1 ADD NEW PASSOWRD 'p4';
-- succeeds; 'p2' and 'p4' can be used to login

-- Set the valid-until of 'new' (p4) password
ALTER ROLE u1 VALID UNTIL '2024/01/01';

-- If we need the ability to change valid-until of both old and new,
we may allow something like the following.
ALTER ROLE u1 [_NEW_ | OLD] VALID UNTIL '2024/01/01';

This way there's a notion of a 'new' and 'old' passwords. User cannot
add a third password without explicitly dropping one of existing
passwords (either old or new). At any time the user can choose to drop
the old or the new password. Adding a new password will mark the
current password as 'old'; if there's only old password (because 'new'
was dropped) the 'old' password will remain intact and the new one
will be placed in 'current'/new spot.

So in normal course of operation, even for automated jobs, the
expected flow to roll over the passwords would be:

ALTER USER u1 DROP OLD PASSWORD;
-- success if there is an old password; otherwise NOTICE: no old password
ALTER USER u1 ADD NEW PASSWORD 'new-secret';

The thing I like about Gurjeet's proposal is that it's well-targeted at
a specific use case rather than trying to be too general. That makes it
a little easier to avoid certain problems like having a process that
adds passwords and never removes the old ones (leading to weird
problems like 47000 passwords for one user).

But it also feels strange to be limited to two -- perhaps the password
rotation schedule or policy just doesn't work with a limit of two, or
perhaps that introduces new kinds of mistakes.

Another idea: what if we introduce the notion of deprecating a
password?

I'll have to think more about it, but perhaps my above proposal
addresses the use case you describe.

if we allow multiple deprecated passwords, we'd still have to come up
with some way to address them (names, numbers, validity period,
something). But by isolating the problem to deprecated passwords only,
it feels like the system is still being restored to a clean state with
at most one single current password. The awkwardness is contained to
old passwords which will hopefully go away soon anyway and not
represent permanent clutter.

+1

Best regards,
Gurjeet
http://Gurje.et

#18Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#15)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Thu, Oct 05, 2023 at 01:09:36PM -0700, Jeff Davis wrote:

On Thu, 2023-10-05 at 14:04 -0500, Nathan Bossart wrote:

That way, we needn't restrict this feature to 2 passwords for
everyone.� Perhaps 2 should be the default, but in any case, IMO we
shouldn't design to only support 2.

Are there use cases for lots of passwords, or is it just a matter of
not introducing an artificial limitation?

I guess it's more of the latter. Perhaps one potential use case would be
short-lived credentials that are created on demand. Such a password might
only be valid for something like 15 minutes, and many users might have the
ability to request a password for the database role. I don't know whether
there is a ton of demand for such a use case, and it might already be
solvable by just creating separate roles. In any case, if there's general
agreement that we only want to target the rotation use case, that's fine by
me.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#19Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#18)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Fri, 2023-10-06 at 14:26 -0500, Nathan Bossart wrote:

I guess it's more of the latter.  Perhaps one potential use case
would be
short-lived credentials that are created on demand.  Such a password
might
only be valid for something like 15 minutes, and many users might
have the
ability to request a password for the database role.  I don't know
whether
there is a ton of demand for such a use case, and it might already be
solvable by just creating separate roles.  In any case, if there's
general
agreement that we only want to target the rotation use case, that's
fine by
me.

The basic problem, as I see it, is: how do we keep users from
accidentally dropping the wrong password? Generated unique names or
numbers don't solve that problem. Auto-incrementing or a created-at
timestamp solves it in the sense that you can at least look at a system
view and see if there's a newer one, but it's a little awkward. A
validity period is a good fit if all passwords have a validity period
and we don't change it, but gets awkward otherwise.

I'm also worried about two kinds of clutter:

* old passwords not being garbage-collected
* the identifier of the current password always changing (perhaps fine
if it'a a "created at" ID?)

Regards,
Jeff Davis

#20Jeff Davis
pgsql@j-davis.com
In reply to: Gurjeet Singh (#17)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Thu, 2023-10-05 at 14:28 -0700, Gurjeet Singh wrote:

This way there's a notion of a 'new' and 'old' passwords.

IIUC, you are proposing that there are exactly two slots, NEW and OLD.
When adding a password, OLD must be unset and it moves NEW to OLD, and
adds the new password in NEW. DROP only works on OLD. Is that right?

It's close to the idea of deprecation, except that adding a new
password implicitly deprecates the existing one. I'm not sure about
that -- it could be confusing.

We could also try using a verb like "expire" that could be coupled with
a date, and that way all old passwords would always have some validity
period. That might make it a bit easier to manage if we do need more
than two passwords.

Regards,
Jeff Davis

#21Bruce Momjian
bruce@momjian.us
In reply to: Jeff Davis (#19)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Fri, Oct 6, 2023 at 01:20:03PM -0700, Jeff Davis wrote:

The basic problem, as I see it, is: how do we keep users from
accidentally dropping the wrong password? Generated unique names or

I thought we could auto-remove old password if the valid-until date is
in the past. You would need a separate ALTER command to sets its date
in the past without that. Also, defining a new password could require
setting the expiration date of the old password to make future additions
easier.

For pg_authid, I was thinking of columns:

ADD rolpassword_old
ADD rolvaliduntil_old
EXISTS rolpassword
EXISTS rolvaliduntil

I did blog about the password rotation problem and suggested
certificates:

https://momjian.us/main/blogs/pgblog/2020.html#July_17_2020

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#22Gurjeet Singh
gurjeet@singh.im
In reply to: Bruce Momjian (#21)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Fri, Oct 6, 2023 at 1:46 PM Bruce Momjian <bruce@momjian.us> wrote:

On Fri, Oct 6, 2023 at 01:20:03PM -0700, Jeff Davis wrote:

The basic problem, as I see it, is: how do we keep users from
accidentally dropping the wrong password? Generated unique names or

I thought we could auto-remove old password if the valid-until date is
in the past.

Autoremoving expired passwords will surprise users, and not in a good
way. Making a password, even an expired one, disappear from the system
will lead to astonishment. Among uses of an expired password are cases
of it acting like a tombstone, and the case where the user may want to
extend the validity of a password, instead of having to create a new
one and change application configuration(s) to specify the new
password.

Best regards,
Gurjeet
http://Gurje.et

#23Bruce Momjian
bruce@momjian.us
In reply to: Gurjeet Singh (#22)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Sun, Oct 8, 2023 at 10:24:42AM -0700, Gurjeet Singh wrote:

On Fri, Oct 6, 2023 at 1:46 PM Bruce Momjian <bruce@momjian.us> wrote:

On Fri, Oct 6, 2023 at 01:20:03PM -0700, Jeff Davis wrote:

The basic problem, as I see it, is: how do we keep users from
accidentally dropping the wrong password? Generated unique names or

I thought we could auto-remove old password if the valid-until date is
in the past.

Autoremoving expired passwords will surprise users, and not in a good
way. Making a password, even an expired one, disappear from the system
will lead to astonishment. Among uses of an expired password are cases
of it acting like a tombstone, and the case where the user may want to
extend the validity of a password, instead of having to create a new
one and change application configuration(s) to specify the new
password.

I was speaking of autoremoving in cases where we are creating a new one,
and taking the previous new one and making it the old one, if that was
not clear.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#24Gurjeet Singh
gurjeet@singh.im
In reply to: Bruce Momjian (#23)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Sun, Oct 8, 2023 at 10:29 AM Bruce Momjian <bruce@momjian.us> wrote:

I was speaking of autoremoving in cases where we are creating a new one,
and taking the previous new one and making it the old one, if that was
not clear.

Yes, I think I understood it differently. I understood it to mean that
this behaviour would apply to all passwords, those created by existing
commands, as well as to those created by new commands for rollover use
case. Whereas you meant this autoremove behaviour to apply only to
those passwords created by/for rollover related commands. I hope I've
understood your proposal correctly this time around :-)

I believe the passwords created by rollover feature should
behave by the same rules as the rules for passwords created by
existing CREATE/ALTER ROLE commands. If we implement the behaviour to
delete expired passwords, then I believe that behaviour should apply
to all passwords, irrespective of which command/feature was used to
create a password.

Best regards,
Gurjeet
http://Gurje.et

#25Bruce Momjian
bruce@momjian.us
In reply to: Gurjeet Singh (#24)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Sun, Oct 8, 2023 at 10:50:15AM -0700, Gurjeet Singh wrote:

On Sun, Oct 8, 2023 at 10:29 AM Bruce Momjian <bruce@momjian.us> wrote:

I was speaking of autoremoving in cases where we are creating a new one,
and taking the previous new one and making it the old one, if that was
not clear.

Yes, I think I understood it differently. I understood it to mean that
this behaviour would apply to all passwords, those created by existing
commands, as well as to those created by new commands for rollover use
case. Whereas you meant this autoremove behaviour to apply only to
those passwords created by/for rollover related commands. I hope I've
understood your proposal correctly this time around :-)

Yes, it is only during the addition of a new password when the previous
new password becomes the new old password. The previous old password
would need to have an rolvaliduntil in the past.

I believe the passwords created by rollover feature should
behave by the same rules as the rules for passwords created by
existing CREATE/ALTER ROLE commands. If we implement the behaviour to
delete expired passwords, then I believe that behaviour should apply
to all passwords, irrespective of which command/feature was used to
create a password.

This would only apply when we are moving the previous new password to
old and the old one is removed.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#26Gurjeet Singh
gurjeet@singh.im
In reply to: Jeff Davis (#20)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Fri, Oct 6, 2023 at 1:29 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Thu, 2023-10-05 at 14:28 -0700, Gurjeet Singh wrote:

This way there's a notion of a 'new' and 'old' passwords.

IIUC, you are proposing that there are exactly two slots, NEW and OLD.
When adding a password, OLD must be unset and it moves NEW to OLD, and
adds the new password in NEW. DROP only works on OLD. Is that right?

Yes, that's what I was proposing. But thinking a bit more about it,
the _implicit_ shuffling of labels 'new' and 'old' doesn't feel right
to me. The password that used to be referred to as 'new' now
automatically gets labeled 'old'.

It's close to the idea of deprecation, except that adding a new
password implicitly deprecates the existing one. I'm not sure about
that -- it could be confusing.

+1

We could also try using a verb like "expire" that could be coupled with
a date, and that way all old passwords would always have some validity
period.

Forcing the users to pick an expiry date for a password they intend to
rollover, when an expiry date did not exist before for that password,
feels like adding more burden to their password rollover decision
making. The dates and rules of password rollover may be a part of a
system external to their database, (wiki, docs, calendar, etc.) which
now they will be forced to translate into a timestamp to specify in
the rollover commands.

I believe we should fix the _names_ of the slots the 2 passwords are
stored in, and provide commands that manipulate those slots by
respective names; the commands should not implicitly move the
passwords between the slots. Additionally, we may provide functions
that provide observability info for these slots. I propose the slot
names FIRST and SECOND (I picked these because these keywords/tokens
already exist in the grammar, but not yet sure if the grammar rules
would allow their use; feel free to propose better names). FIRST
refers to the the existing slot, namely rolpassword. SECOND refers to
the new slot we'd add, that is, a pgauthid column named
rolsecondpassword. The existing commands, or when neither FIRST nor
SECOND are specified, the commands apply to the existing slot, a.k.a.
FIRST.

The user interface might look like the following:

-- Create a user, as usual
CREATE ROLE u1 PASSWORD 'p1' VALID UNTIL '2020/01/01';
-- This automatically occupies the 'first' slot

-- Add another password that the user can use for authentication.
ALTER ROLE u1 ADD SECOND PASSWORD 'p2' VALID UNTIL '2021/01/01';

-- Change both the passwords' validity independently; this solves the
-- problem with the previous '2-element stack' approach, where we
-- could not address the password at the bottom of the stack.
ALTER ROLE u1 SECOND PASSWORD VALID UNTIL '2022/01/01';

ALTER ROLE u1 [ [ FIRST ] PASSWORD ] VALID UNTIL '2022/01/01';

-- If, for some reason, the user wants to get rid of the latest password added.
ALTER ROLE u1 DROP SECOND PASSWORD;

-- Add a new password (p3) in 'second' slot
ALTER ROLE u1 ADD SECOND PASSWORD 'p3' VALID UNTIL '2023/01/01';

-- Attempting to add a password while the respective slot is occupied
-- results in error
ALTER ROLE u1 ADD [ [ FIRST ] PASSWORD ] 'p4' VALID UNTIL '2024/01/01';
ERROR: first password already exists

ALTER ROLE u1 ADD SECOND PASSWORD 'p4' VALID UNTIL '2024/01/01';
ERROR: second password already exists

-- Users can use this function to check whether a password slot is occupied
=> select password_exists('u1', 'first');
password_exists
-----
t

=> select password_exists('u1', 'second');
password_exists
-----
t

-- Remove all passwords from the role. Both, 'first' and 'second',
passwords are removed.
ALTER ROLE u1 DROP ALL PASSWORD;

Best regards,
Gurjeet
http://Gurje.et

#27Gurjeet Singh
gurjeet@singh.im
In reply to: Gurjeet Singh (#26)
1 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Sun, Oct 8, 2023 at 1:01 PM Gurjeet Singh <gurjeet@singh.im> wrote:

On Fri, Oct 6, 2023 at 1:29 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Thu, 2023-10-05 at 14:28 -0700, Gurjeet Singh wrote:

This way there's a notion of a 'new' and 'old' passwords.

IIUC, you are proposing that there are exactly two slots, NEW and OLD.
When adding a password, OLD must be unset and it moves NEW to OLD, and
adds the new password in NEW. DROP only works on OLD. Is that right?

Yes, that's what I was proposing. But thinking a bit more about it,
the _implicit_ shuffling of labels 'new' and 'old' doesn't feel right
to me. The password that used to be referred to as 'new' now
automatically gets labeled 'old'.

It's close to the idea of deprecation, except that adding a new
password implicitly deprecates the existing one. I'm not sure about
that -- it could be confusing.

+1

I believe we should fix the _names_ of the slots the 2 passwords are
stored in, and provide commands that manipulate those slots by
respective names; the commands should not implicitly move the
passwords between the slots. Additionally, we may provide functions
that provide observability info for these slots. I propose the slot
names FIRST and SECOND (I picked these because these keywords/tokens
already exist in the grammar, but not yet sure if the grammar rules
would allow their use; feel free to propose better names). FIRST
refers to the the existing slot, namely rolpassword. SECOND refers to
the new slot we'd add, that is, a pgauthid column named
rolsecondpassword. The existing commands, or when neither FIRST nor
SECOND are specified, the commands apply to the existing slot, a.k.a.
FIRST.

Please see attached the patch that implements this proposal. The patch
is named password_rollover_v3.diff, breaking from the name
'multiple_passwords...', since this patch limits itself to address the
password-rollover feature.

The multiple_password* series of patches had removed a critical
functionality, which I believe is crucial from security perspective.
When a user does not exist, or has no passwords, or has passwords that
have expired, we must pretend to perform authentication (network
packet exchanges) as normally as possible, so that the absence of
user, or lack of (or expiration of) passwords is not revealed to an
attacker. I have restored the original behaviour in the
CheckPWChallengeAuth() function; see commit aba99df407 [2]https://github.com/gurjeet/postgres/commit/aba99df407a523357db2813f0eea0b45dbeb6006.

I looked for any existing keywords that may better fit the purpose of
naming the slots, better than FIRST and SECOND, but I could not find
any. Instead of DROP to remove the passwords, I tried DELETE and the
grammar/bison did not complain about it; so DELETE is an option, too,
but I feel DROP FIRST/SECOND/ALL PASSWORD is a better companion of ADD
FIRST/SECOND PASSWORD syntax, in the same vein as ADD/DROP COLUMN.

The doc changes are still missing, but the regression tests and the
comments therein should provide a good idea of the user interface of
this new feature. Documenting this behaviour in a succinct manner
feels difficult; so ideas welcome for how to inform the reader that
now a role is accompanied by two slots to store the passwords, and
that the old commands operate on the first slot, and to operate on the
second password slot one must use the new syntax. I guess it would be
best to start the relevant section with "To support gradual password
rollovers, Postgres provides the ability to store 2 active passwords
at the same time. The passwords are referred to as FIRST and SECOND
password. Each of these passwords can be changed independently, and
each of these can have an associated expiration time, if necessary."

Since these new commands are only available to ALTER ROLE (and not to
CREATE ROLE), the corresponding command doc page also needs to be
updated.

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

The patch progress can be followed on the Git branch
password_rollover_v3 [1]https://github.com/gurjeet/postgres/commits/password_rollover_v3. This branch begins uses
multiple_passwords_v4 as starting point, and removes unnecessary code
(commit 326f60225f [3]https://github.com/gurjeet/postgres/commit/326f60225f0e660338fc9c276c8728dc10db435b)

The v1 (and tombstone of v2) patches of password_rollover never
finished as the consensus changed while they were in progress, but
they exist as sibling branches of the v3 branch.

[1]: https://github.com/gurjeet/postgres/commits/password_rollover_v3
[2]: https://github.com/gurjeet/postgres/commit/aba99df407a523357db2813f0eea0b45dbeb6006
[3]: https://github.com/gurjeet/postgres/commit/326f60225f0e660338fc9c276c8728dc10db435b

Best regards,
Gurjeet
http://Gurje.et

Attachments:

password_rollover_v3.diffapplication/octet-stream; name=password_rollover_v3.diffDownload
diff --git a/.cirrus.star b/.cirrus.star
index 36233872d1..d2d6ceca20 100644
--- a/.cirrus.star
+++ b/.cirrus.star
@@ -46,7 +46,7 @@ def main():
 
 def config_from(config_src):
     """return contents of config file `config_src`, surrounded by markers
-    indicating start / end of the included file
+    indicating start / end of the the included file
     """
 
     config_contents = fs.read(config_src)
diff --git a/contrib/citext/expected/citext_utf8.out b/contrib/citext/expected/citext_utf8.out
index 5d988dcd48..6630e09a4d 100644
--- a/contrib/citext/expected/citext_utf8.out
+++ b/contrib/citext/expected/citext_utf8.out
@@ -2,7 +2,7 @@
  * This test must be run in a database with UTF-8 encoding
  * and a Unicode-aware locale.
  *
- * Also disable this file for ICU, because the test for the
+ * Also disable this file for ICU, because the test for the the
  * Turkish dotted I is not correct for many ICU locales. citext always
  * uses the default collation, so it's not easy to restrict the test
  * to the "tr-TR-x-icu" collation where it will succeed.
diff --git a/contrib/citext/expected/citext_utf8_1.out b/contrib/citext/expected/citext_utf8_1.out
index 7065a5da19..3caa7a00d4 100644
--- a/contrib/citext/expected/citext_utf8_1.out
+++ b/contrib/citext/expected/citext_utf8_1.out
@@ -2,7 +2,7 @@
  * This test must be run in a database with UTF-8 encoding
  * and a Unicode-aware locale.
  *
- * Also disable this file for ICU, because the test for the
+ * Also disable this file for ICU, because the test for the the
  * Turkish dotted I is not correct for many ICU locales. citext always
  * uses the default collation, so it's not easy to restrict the test
  * to the "tr-TR-x-icu" collation where it will succeed.
diff --git a/contrib/citext/sql/citext_utf8.sql b/contrib/citext/sql/citext_utf8.sql
index 34b232d64e..1f51df134b 100644
--- a/contrib/citext/sql/citext_utf8.sql
+++ b/contrib/citext/sql/citext_utf8.sql
@@ -2,7 +2,7 @@
  * This test must be run in a database with UTF-8 encoding
  * and a Unicode-aware locale.
  *
- * Also disable this file for ICU, because the test for the
+ * Also disable this file for ICU, because the test for the the
  * Turkish dotted I is not correct for many ICU locales. citext always
  * uses the default collation, so it's not easy to restrict the test
  * to the "tr-TR-x-icu" collation where it will succeed.
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 3b2fa1129e..fbf8ad669e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1788,7 +1788,7 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
   </para>
 
   <para>
-   To create a subscription, the user must have the privileges of
+   To create a subscription, the user must have the privileges of the
    the <literal>pg_create_subscription</literal> role, as well as
    <literal>CREATE</literal> privileges on the database.
   </para>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index c1bafbfa06..71652fd918 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -51,7 +51,7 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
   </para>
 
   <para>
-   To be able to create a subscription, you must have the privileges of
+   To be able to create a subscription, you must have the privileges of the
    the <literal>pg_create_subscription</literal> role, as well as
    <literal>CREATE</literal> privileges on the current database.
   </para>
diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c
index efc5284e5b..72b052b249 100644
--- a/src/backend/access/nbtree/nbtsearch.c
+++ b/src/backend/access/nbtree/nbtsearch.c
@@ -1664,7 +1664,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum)
 
 			/*
 			 * If the result of prechecking required keys was true, then in
-			 * assert-enabled builds we also recheck that the _bt_checkkeys()
+			 * assert-enabled builds we also recheck that _bt_checkkeys()
 			 * result is the same.
 			 */
 			Assert(!requiredMatchedByPrecheck ||
@@ -1783,7 +1783,7 @@ _bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum)
 
 			/*
 			 * If the result of prechecking required keys was true, then in
-			 * assert-enabled builds we also recheck that the _bt_checkkeys()
+			 * assert-enabled builds we also recheck that _bt_checkkeys()
 			 * result is the same.
 			 */
 			Assert(!requiredMatchedByPrecheck ||
diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c
index e0baa86bd3..a1363e3b8f 100644
--- a/src/backend/access/transam/xlogreader.c
+++ b/src/backend/access/transam/xlogreader.c
@@ -846,7 +846,7 @@ restart:
 
 	/*
 	 * If we got here without a DecodedXLogRecord, it means we needed to
-	 * validate total_len before trusting it, but by now we've done that.
+	 * validate total_len before trusting it, but by now now we've done that.
 	 */
 	if (decoded == NULL)
 	{
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index ce77a055e5..4721185e71 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"
@@ -125,6 +127,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
@@ -153,8 +235,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;
@@ -442,19 +524,28 @@ 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
 		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);
 
 	/*
@@ -630,11 +721,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;
@@ -644,10 +740,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."));
@@ -659,9 +763,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)
 		{
@@ -718,6 +908,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)
@@ -731,6 +933,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);
@@ -739,8 +943,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.
@@ -768,7 +994,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() ||
@@ -776,7 +1002,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"),
@@ -784,7 +1010,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"),
@@ -843,15 +1069,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
@@ -917,6 +1170,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)
@@ -927,26 +1194,85 @@ 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);
 		}
 		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;
+	}
+
 	/* 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)
 	{
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 25a2d78f15..aea44a9d56 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -1306,7 +1306,7 @@ ExecParallelHashJoinNewBatch(HashJoinState *hjstate)
  * The data recorded in the file for each tuple is its hash value,
  * then the tuple in MinimalTuple format.
  *
- * fileptr points to a batch file in one of the hashtable arrays.
+ * fileptr points to a batch file in one of the the hashtable arrays.
  *
  * The batch files (and their buffers) are allocated in the spill context
  * created for the hashtable.
diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c
index c535bc5383..56bd165885 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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 118d15b1a1..a0ebfecd69 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,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;
@@ -145,10 +151,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;
@@ -231,17 +243,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.
+ * 'passwords' are the role's stored secrets, 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.
  */
 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;
@@ -270,49 +285,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;
 	}
 
 	/*
@@ -323,10 +343,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;
 	}
 
@@ -474,7 +497,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -491,11 +514,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,
@@ -1142,48 +1174,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;
+		}
+
+		// TODO: Convert to DEBUG2
+		elog(LOG, "succeeded on %d password", j);
+
+		pg_hmac_free(ctx);
+
+		/* 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];
+
+		/* 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);
+
+		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;
+		}
 	}
 
-	pg_hmac_free(ctx);
-
-	/* 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];
-
-	/* 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);
-
-	if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0)
-		return false;
-
-	return true;
+	return false;
 }
 
 /*
@@ -1409,7 +1455,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 81dabb9c27..c6a90939cb 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -57,8 +57,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);
 
 
 /*----------------------------------------------------------------
@@ -789,8 +788,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);
 
@@ -798,17 +798,21 @@ 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)
-	{
-		result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
-									logdetail);
-	}
-	else
-		result = STATUS_ERROR;
+	passwords = get_role_passwords(port->user_name, logdetail, &num_passwords);
+	if (passwords != NULL) {
+		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);
+	}
 
-	if (shadow_pass)
-		pfree(shadow_pass);
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -823,54 +827,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);
+		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_password() returned error, return error, even if the
+	 * If get_role_passwords() returned error, return error, even if the
 	 * authentication succeeded.
 	 */
-	if (!shadow_pass)
+	if (!passwords)
 	{
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
@@ -883,11 +914,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))
@@ -903,12 +935,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 ef496a0bea..3b47af1269 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,20 +27,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;
+	Datum		second_datum;
+	bool		vuntil_isnull;
+	bool		second_vuntil_isnull;
+	bool		password_isnull;
+	bool		second_password_isnull;
 	char	   *shadow_pass;
+	char	   *second_shadow_pass;
+
+	*num_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,34 +61,73 @@ 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;
 }
 
 /*
@@ -113,7 +161,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);
@@ -134,13 +182,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'");
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 7af001feaa..4012901626 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -563,7 +563,7 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 	set_cheapest(rel);
 
 #ifdef OPTIMIZER_DEBUG
-	pprint(rel);
+	debug_print_rel(root, rel);
 #endif
 }
 
@@ -3504,7 +3504,7 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels)
 			set_cheapest(rel);
 
 #ifdef OPTIMIZER_DEBUG
-			pprint(rel);
+			debug_print_rel(root, rel);
 #endif
 		}
 	}
@@ -4372,7 +4372,7 @@ generate_partitionwise_join_paths(PlannerInfo *root, RelOptInfo *rel)
 			continue;
 
 #ifdef OPTIMIZER_DEBUG
-		pprint(child_rel);
+		debug_print_rel(root, child_rel);
 #endif
 
 		live_children = lappend(live_children, child_rel);
@@ -4389,3 +4389,325 @@ generate_partitionwise_join_paths(PlannerInfo *root, RelOptInfo *rel)
 	add_paths_to_append_rel(root, rel, live_children);
 	list_free(live_children);
 }
+
+
+/*****************************************************************************
+ *			DEBUG SUPPORT
+ *****************************************************************************/
+
+#ifdef OPTIMIZER_DEBUG
+
+static void
+print_relids(PlannerInfo *root, Relids relids)
+{
+	int			x;
+	bool		first = true;
+
+	x = -1;
+	while ((x = bms_next_member(relids, x)) >= 0)
+	{
+		if (!first)
+			printf(" ");
+		if (x < root->simple_rel_array_size &&
+			root->simple_rte_array[x])
+			printf("%s", root->simple_rte_array[x]->eref->aliasname);
+		else
+			printf("%d", x);
+		first = false;
+	}
+}
+
+static void
+print_restrictclauses(PlannerInfo *root, List *clauses)
+{
+	ListCell   *l;
+
+	foreach(l, clauses)
+	{
+		RestrictInfo *c = lfirst(l);
+
+		print_expr((Node *) c->clause, root->parse->rtable);
+		if (lnext(clauses, l))
+			printf(", ");
+	}
+}
+
+static void
+print_path(PlannerInfo *root, Path *path, int indent)
+{
+	const char *ptype;
+	bool		join = false;
+	Path	   *subpath = NULL;
+	int			i;
+
+	switch (nodeTag(path))
+	{
+		case T_Path:
+			switch (path->pathtype)
+			{
+				case T_SeqScan:
+					ptype = "SeqScan";
+					break;
+				case T_SampleScan:
+					ptype = "SampleScan";
+					break;
+				case T_FunctionScan:
+					ptype = "FunctionScan";
+					break;
+				case T_TableFuncScan:
+					ptype = "TableFuncScan";
+					break;
+				case T_ValuesScan:
+					ptype = "ValuesScan";
+					break;
+				case T_CteScan:
+					ptype = "CteScan";
+					break;
+				case T_NamedTuplestoreScan:
+					ptype = "NamedTuplestoreScan";
+					break;
+				case T_Result:
+					ptype = "Result";
+					break;
+				case T_WorkTableScan:
+					ptype = "WorkTableScan";
+					break;
+				default:
+					ptype = "???Path";
+					break;
+			}
+			break;
+		case T_IndexPath:
+			ptype = "IdxScan";
+			break;
+		case T_BitmapHeapPath:
+			ptype = "BitmapHeapScan";
+			break;
+		case T_BitmapAndPath:
+			ptype = "BitmapAndPath";
+			break;
+		case T_BitmapOrPath:
+			ptype = "BitmapOrPath";
+			break;
+		case T_TidPath:
+			ptype = "TidScan";
+			break;
+		case T_TidRangePath:
+			ptype = "TidRangePath";
+			break;
+		case T_SubqueryScanPath:
+			ptype = "SubqueryScan";
+			break;
+		case T_ForeignPath:
+			ptype = "ForeignScan";
+			break;
+		case T_CustomPath:
+			ptype = "CustomScan";
+			break;
+		case T_NestPath:
+			ptype = "NestLoop";
+			join = true;
+			break;
+		case T_MergePath:
+			ptype = "MergeJoin";
+			join = true;
+			break;
+		case T_HashPath:
+			ptype = "HashJoin";
+			join = true;
+			break;
+		case T_AppendPath:
+			ptype = "Append";
+			break;
+		case T_MergeAppendPath:
+			ptype = "MergeAppend";
+			break;
+		case T_GroupResultPath:
+			ptype = "GroupResult";
+			break;
+		case T_MaterialPath:
+			ptype = "Material";
+			subpath = ((MaterialPath *) path)->subpath;
+			break;
+		case T_MemoizePath:
+			ptype = "Memoize";
+			subpath = ((MemoizePath *) path)->subpath;
+			break;
+		case T_UniquePath:
+			ptype = "Unique";
+			subpath = ((UniquePath *) path)->subpath;
+			break;
+		case T_GatherPath:
+			ptype = "Gather";
+			subpath = ((GatherPath *) path)->subpath;
+			break;
+		case T_GatherMergePath:
+			ptype = "GatherMerge";
+			subpath = ((GatherMergePath *) path)->subpath;
+			break;
+		case T_ProjectionPath:
+			ptype = "Projection";
+			subpath = ((ProjectionPath *) path)->subpath;
+			break;
+		case T_ProjectSetPath:
+			ptype = "ProjectSet";
+			subpath = ((ProjectSetPath *) path)->subpath;
+			break;
+		case T_SortPath:
+			ptype = "Sort";
+			subpath = ((SortPath *) path)->subpath;
+			break;
+		case T_IncrementalSortPath:
+			ptype = "IncrementalSort";
+			subpath = ((SortPath *) path)->subpath;
+			break;
+		case T_GroupPath:
+			ptype = "Group";
+			subpath = ((GroupPath *) path)->subpath;
+			break;
+		case T_UpperUniquePath:
+			ptype = "UpperUnique";
+			subpath = ((UpperUniquePath *) path)->subpath;
+			break;
+		case T_AggPath:
+			ptype = "Agg";
+			subpath = ((AggPath *) path)->subpath;
+			break;
+		case T_GroupingSetsPath:
+			ptype = "GroupingSets";
+			subpath = ((GroupingSetsPath *) path)->subpath;
+			break;
+		case T_MinMaxAggPath:
+			ptype = "MinMaxAgg";
+			break;
+		case T_WindowAggPath:
+			ptype = "WindowAgg";
+			subpath = ((WindowAggPath *) path)->subpath;
+			break;
+		case T_SetOpPath:
+			ptype = "SetOp";
+			subpath = ((SetOpPath *) path)->subpath;
+			break;
+		case T_RecursiveUnionPath:
+			ptype = "RecursiveUnion";
+			break;
+		case T_LockRowsPath:
+			ptype = "LockRows";
+			subpath = ((LockRowsPath *) path)->subpath;
+			break;
+		case T_ModifyTablePath:
+			ptype = "ModifyTable";
+			break;
+		case T_LimitPath:
+			ptype = "Limit";
+			subpath = ((LimitPath *) path)->subpath;
+			break;
+		default:
+			ptype = "???Path";
+			break;
+	}
+
+	for (i = 0; i < indent; i++)
+		printf("\t");
+	printf("%s", ptype);
+
+	if (path->parent)
+	{
+		printf("(");
+		print_relids(root, path->parent->relids);
+		printf(")");
+	}
+	if (path->param_info)
+	{
+		printf(" required_outer (");
+		print_relids(root, path->param_info->ppi_req_outer);
+		printf(")");
+	}
+	printf(" rows=%.0f cost=%.2f..%.2f\n",
+		   path->rows, path->startup_cost, path->total_cost);
+
+	if (path->pathkeys)
+	{
+		for (i = 0; i < indent; i++)
+			printf("\t");
+		printf("  pathkeys: ");
+		print_pathkeys(path->pathkeys, root->parse->rtable);
+	}
+
+	if (join)
+	{
+		JoinPath   *jp = (JoinPath *) path;
+
+		for (i = 0; i < indent; i++)
+			printf("\t");
+		printf("  clauses: ");
+		print_restrictclauses(root, jp->joinrestrictinfo);
+		printf("\n");
+
+		if (IsA(path, MergePath))
+		{
+			MergePath  *mp = (MergePath *) path;
+
+			for (i = 0; i < indent; i++)
+				printf("\t");
+			printf("  sortouter=%d sortinner=%d materializeinner=%d\n",
+				   ((mp->outersortkeys) ? 1 : 0),
+				   ((mp->innersortkeys) ? 1 : 0),
+				   ((mp->materialize_inner) ? 1 : 0));
+		}
+
+		print_path(root, jp->outerjoinpath, indent + 1);
+		print_path(root, jp->innerjoinpath, indent + 1);
+	}
+
+	if (subpath)
+		print_path(root, subpath, indent + 1);
+}
+
+void
+debug_print_rel(PlannerInfo *root, RelOptInfo *rel)
+{
+	ListCell   *l;
+
+	printf("RELOPTINFO (");
+	print_relids(root, rel->relids);
+	printf("): rows=%.0f width=%d\n", rel->rows, rel->reltarget->width);
+
+	if (rel->baserestrictinfo)
+	{
+		printf("\tbaserestrictinfo: ");
+		print_restrictclauses(root, rel->baserestrictinfo);
+		printf("\n");
+	}
+
+	if (rel->joininfo)
+	{
+		printf("\tjoininfo: ");
+		print_restrictclauses(root, rel->joininfo);
+		printf("\n");
+	}
+
+	printf("\tpath list:\n");
+	foreach(l, rel->pathlist)
+		print_path(root, lfirst(l), 1);
+	if (rel->cheapest_parameterized_paths)
+	{
+		printf("\n\tcheapest parameterized paths:\n");
+		foreach(l, rel->cheapest_parameterized_paths)
+			print_path(root, lfirst(l), 1);
+	}
+	if (rel->cheapest_startup_path)
+	{
+		printf("\n\tcheapest startup path:\n");
+		print_path(root, rel->cheapest_startup_path, 1);
+	}
+	if (rel->cheapest_total_path)
+	{
+		printf("\n\tcheapest total path:\n");
+		print_path(root, rel->cheapest_total_path, 1);
+	}
+	printf("\n");
+	fflush(stdout);
+}
+
+#endif							/* OPTIMIZER_DEBUG */
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b65323532b..211ba65389 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3133,26 +3133,10 @@ create_agg_path(PlannerInfo *root,
 	pathnode->path.parallel_safe = rel->consider_parallel &&
 		subpath->parallel_safe;
 	pathnode->path.parallel_workers = subpath->parallel_workers;
-
 	if (aggstrategy == AGG_SORTED)
-	{
-		/*
-		 * Attempt to preserve the order of the subpath.  Additional pathkeys
-		 * may have been added in adjust_group_pathkeys_for_groupagg() to
-		 * support ORDER BY / DISTINCT aggregates.  Pathkeys added there
-		 * belong to columns within the aggregate function, so we must strip
-		 * these additional pathkeys off as those columns are unavailable
-		 * above the aggregate node.
-		 */
-		if (list_length(subpath->pathkeys) > root->num_groupby_pathkeys)
-			pathnode->path.pathkeys = list_copy_head(subpath->pathkeys,
-													 root->num_groupby_pathkeys);
-		else
-			pathnode->path.pathkeys = subpath->pathkeys;	/* preserves order */
-	}
+		pathnode->path.pathkeys = subpath->pathkeys;	/* preserves order */
 	else
 		pathnode->path.pathkeys = NIL;	/* output is unordered */
-
 	pathnode->subpath = subpath;
 
 	pathnode->aggstrategy = aggstrategy;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e56cbe77cb..6447ac4056 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -361,7 +361,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
@@ -1168,6 +1169,7 @@ OptRoleList:
 
 AlterOptRoleList:
 			AlterOptRoleList AlterOptRoleElem		{ $$ = lappend($1, $2); }
+			| AlterOptRoleList AlterOnlyOptRoleElem	{ $$ = lappend($1, $2); }
 			| /* EMPTY */							{ $$ = NIL; }
 		;
 
@@ -1263,6 +1265,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/backend/utils/adt/array_userfuncs.c b/src/backend/utils/adt/array_userfuncs.c
index 7f87df45df..5c4fdcfba4 100644
--- a/src/backend/utils/adt/array_userfuncs.c
+++ b/src/backend/utils/adt/array_userfuncs.c
@@ -723,13 +723,12 @@ array_agg_deserialize(PG_FUNCTION_ARGS)
 	sstate = PG_GETARG_BYTEA_PP(0);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	/* element_type */
 	element_type = pq_getmsgint(&buf, 4);
@@ -826,6 +825,7 @@ array_agg_deserialize(PG_FUNCTION_ARGS)
 	}
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	PG_RETURN_POINTER(result);
 }
@@ -1134,13 +1134,12 @@ array_agg_array_deserialize(PG_FUNCTION_ARGS)
 	sstate = PG_GETARG_BYTEA_PP(0);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	/* element_type */
 	element_type = pq_getmsgint(&buf, 4);
@@ -1198,6 +1197,7 @@ array_agg_array_deserialize(PG_FUNCTION_ARGS)
 	memcpy(result->lbs, temp, sizeof(result->lbs));
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index f4b885005f..3c3184f15b 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -5190,13 +5190,12 @@ numeric_avg_deserialize(PG_FUNCTION_ARGS)
 	init_var(&tmp_var);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	result = makeNumericAggStateCurrentContext(false);
 
@@ -5223,6 +5222,7 @@ numeric_avg_deserialize(PG_FUNCTION_ARGS)
 	result->nInfcount = pq_getmsgint64(&buf);
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	free_var(&tmp_var);
 
@@ -5306,13 +5306,12 @@ numeric_deserialize(PG_FUNCTION_ARGS)
 	init_var(&tmp_var);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	result = makeNumericAggStateCurrentContext(false);
 
@@ -5343,6 +5342,7 @@ numeric_deserialize(PG_FUNCTION_ARGS)
 	result->nInfcount = pq_getmsgint64(&buf);
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	free_var(&tmp_var);
 
@@ -5677,13 +5677,12 @@ numeric_poly_deserialize(PG_FUNCTION_ARGS)
 	init_var(&tmp_var);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	result = makePolyNumAggStateCurrentContext(false);
 
@@ -5707,6 +5706,7 @@ numeric_poly_deserialize(PG_FUNCTION_ARGS)
 #endif
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	free_var(&tmp_var);
 
@@ -5868,13 +5868,12 @@ int8_avg_deserialize(PG_FUNCTION_ARGS)
 	init_var(&tmp_var);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	result = makePolyNumAggStateCurrentContext(false);
 
@@ -5890,6 +5889,7 @@ int8_avg_deserialize(PG_FUNCTION_ARGS)
 #endif
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	free_var(&tmp_var);
 
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 1aff04fa77..72e1e24fe0 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5289,13 +5289,12 @@ string_agg_deserialize(PG_FUNCTION_ARGS)
 	sstate = PG_GETARG_BYTEA_PP(0);
 
 	/*
-	 * Fake up a StringInfo pointing to the bytea's value so we can "receive"
-	 * the serialized aggregate state value.
+	 * Copy the bytea into a StringInfo so that we can "receive" it using the
+	 * standard recv-function infrastructure.
 	 */
-	buf.data = VARDATA_ANY(sstate);
-	buf.len = VARSIZE_ANY_EXHDR(sstate);
-	buf.maxlen = 0;
-	buf.cursor = 0;
+	initStringInfo(&buf);
+	appendBinaryStringInfo(&buf,
+						   VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate));
 
 	result = makeStringAggState(fcinfo);
 
@@ -5308,6 +5307,7 @@ string_agg_deserialize(PG_FUNCTION_ARGS)
 	appendBinaryStringInfo(result, data, datalen);
 
 	pq_getmsgend(&buf);
+	pfree(buf.data);
 
 	PG_RETURN_POINTER(result);
 }
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 16ec6c5ef0..3a1d846db5 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -611,6 +611,7 @@ static char *recovery_target_xid_string;
 static char *recovery_target_name_string;
 static char *recovery_target_lsn_string;
 
+
 /* should be static, but commands/variable.c needs to get at this */
 char	   *role_string;
 
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6b4a0aaaad..b326e48376 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,76 +23,91 @@
   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 => '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 0e7ddc56ea..2a27ae3e10 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;
 
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 97dcb93791..cdc6673357 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -21,6 +21,7 @@
 extern PGDLLIMPORT int Password_encryption; /* values from enum PasswordType */
 extern PGDLLIMPORT char *createrole_self_grant;
 
+
 /* Hook to check passwords in CreateRole() and AlterRole() */
 typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
 
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index ddcd27469a..966eb4e627 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 7a1b1ed0a0..12c5c9602b 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 310bc36517..07d9cc3990 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,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 7b896d821e..50bc3b503a 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -63,6 +63,10 @@ extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel,
 extern void generate_partitionwise_join_paths(PlannerInfo *root,
 											  RelOptInfo *rel);
 
+#ifdef OPTIMIZER_DEBUG
+extern void debug_print_rel(PlannerInfo *root, RelOptInfo *rel);
+#endif
+
 /*
  * indxpath.c
  *	  routines to generate index paths
diff --git a/src/test/regress/expected/password_rollover.out b/src/test/regress/expected/password_rollover.out
new file mode 100644
index 0000000000..bad6d01b61
--- /dev/null
+++ b/src/test/regress/expected/password_rollover.out
@@ -0,0 +1,140 @@
+--
+-- Tests for password rollovers
+--
+SET password_encryption = 'md5';
+-- Create a user, 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 user 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 user 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 USER 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 USER 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';
+ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+-- the rolsecondpassword field should now contain a SCRAM secret
+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 | md5cc8c5dac5560a2fead71cfba4625a2c7 | Sat Jan 01 00:00:00 2022 PST | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> | infinity
+(1 row)
+
+-- Drop the less secure, md5, password
+ALTER USER regress_password_rollover1 DROP FIRST PASSWORD;
+-- 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)
+
diff --git a/src/test/regress/expected/tuplesort.out b/src/test/regress/expected/tuplesort.out
index 0e8b5bf4a3..a2efa179fc 100644
--- a/src/test/regress/expected/tuplesort.out
+++ b/src/test/regress/expected/tuplesort.out
@@ -401,7 +401,7 @@ FETCH NEXT FROM c;
  00000000-0000-0000-0000-000000000000
 (1 row)
 
--- scroll beyond end
+-- scroll beyond end end
 FETCH LAST FROM c;
  noabort_decreasing 
 --------------------
@@ -498,7 +498,7 @@ FETCH NEXT FROM c;
  00000000-0000-0000-0000-000000000000
 (1 row)
 
--- scroll beyond end
+-- scroll beyond end end
 FETCH LAST FROM c;
  noabort_decreasing 
 --------------------
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4df9d8503b..5efad7f3ad 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..73a42f97ab
--- /dev/null
+++ b/src/test/regress/sql/password_rollover.sql
@@ -0,0 +1,107 @@
+--
+-- Tests for password rollovers
+--
+
+SET password_encryption = 'md5';
+
+-- Create a user, 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 user 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 user 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 USER 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 USER 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';
+
+ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+
+-- the rolsecondpassword field should now contain a SCRAM secret
+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%';
+
+-- Drop the less secure, md5, password
+ALTER USER regress_password_rollover1 DROP FIRST PASSWORD;
+
+-- 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%';
diff --git a/src/test/regress/sql/tuplesort.sql b/src/test/regress/sql/tuplesort.sql
index 658fe98dc5..846484d561 100644
--- a/src/test/regress/sql/tuplesort.sql
+++ b/src/test/regress/sql/tuplesort.sql
@@ -169,7 +169,7 @@ FETCH BACKWARD FROM c;
 FETCH BACKWARD FROM c;
 FETCH NEXT FROM c;
 
--- scroll beyond end
+-- scroll beyond end end
 FETCH LAST FROM c;
 FETCH BACKWARD FROM c;
 FETCH NEXT FROM c;
@@ -200,7 +200,7 @@ FETCH BACKWARD FROM c;
 FETCH BACKWARD FROM c;
 FETCH NEXT FROM c;
 
--- scroll beyond end
+-- scroll beyond end end
 FETCH LAST FROM c;
 FETCH BACKWARD FROM c;
 FETCH NEXT FROM c;
#28Gurjeet Singh
gurjeet@singh.im
In reply to: Gurjeet Singh (#27)
3 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <gurjeet@singh.im> wrote:

Next steps:
- Break the patch into a series of smaller patches.

Please see attached the same v3 patch, but now split into 3 separate
patches. Each patch in the series depends on the previous patch to
have been applied. I have made sure that each patch passes `make
check` individually.

First patch adds the two new columns, rolsecondpassword and
rolsecondvaliduntil to the pg_authid shared catalog. This patch also
updates the corresponding pg_authid.dat file to set these values to
null for the rows populated during bootstrap. Finally, it adds code to
CreateRole() to set these columns' values to NULL for a role being
created.

The second patch updates the password extraction, verification
functions as well as authentication functions to honor the second
password, if any. There is more detailed description in the commit
message/body of the patch.

The third patch adds the SQL support to the ALTER ROLE command which
allows manipulation of both, the rolpassword and rolsecondpassword,
columns and their respective expiration timestamps,
rol[second]validuntil. This patch also adds regression tests for the
new SQL command, demonstrating the use of the new grammar.

v3-0001-Add-new-columns-to-pg_authid.patch
v3-0002-Update-password-verification-infrastructure-to-ha.patch
v3-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patch

Best regards,
Gurjeet
http://Gurje.et

Attachments:

v3-0001-Add-new-columns-to-pg_authid.patchapplication/x-patch; name=v3-0001-Add-new-columns-to-pg_authid.patchDownload
From bc7c35e53e421157c9425c198bc2557ad118a650 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:36:05 -0700
Subject: [PATCH v3 1/3] 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 | 45 ++++++++++++++++++++-----------
 src/include/catalog/pg_authid.h   |  2 ++
 3 files changed, 36 insertions(+), 15 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index ce77a055e5..0afaf74865 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -452,9 +452,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 6b4a0aaaad..b326e48376 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,76 +23,91 @@
   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 => '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 0e7ddc56ea..2a27ae3e10 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.41.0

v3-0002-Update-password-verification-infrastructure-to-ha.patchapplication/x-patch; name=v3-0002-Update-password-verification-infrastructure-to-ha.patchDownload
From f8909979c17d64fb14f4074b1b811052d67aba4e Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:48:11 -0700
Subject: [PATCH v3 2/3] 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  |   4 +-
 src/backend/libpq/auth-scram.c | 210 ++++++++++++++++++++-------------
 src/backend/libpq/auth.c       | 114 +++++++++++-------
 src/backend/libpq/crypt.c      |  78 +++++++++---
 src/include/libpq/crypt.h      |   2 +-
 src/include/libpq/sasl.h       |   4 +-
 src/include/libpq/scram.h      |   2 +-
 8 files changed, 374 insertions(+), 152 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 0afaf74865..9ad02e4092 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"
@@ -125,6 +127,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
@@ -153,8 +235,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;
@@ -442,11 +524,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
@@ -772,7 +859,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() ||
@@ -931,9 +1018,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 c535bc5383..56bd165885 100644
--- a/src/backend/libpq/auth-sasl.c
+++ b/src/backend/libpq/auth-sasl.c
@@ -49,7 +49,7 @@
  * 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 118d15b1a1..a0ebfecd69 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,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;
@@ -145,10 +151,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;
@@ -231,17 +243,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.
+ * 'passwords' are the role's stored secrets, 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.
  */
 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;
@@ -270,49 +285,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;
 	}
 
 	/*
@@ -323,10 +343,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;
 	}
 
@@ -474,7 +497,7 @@ scram_exchange(void *opaq, const char *input, int inputlen,
  * 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;
@@ -491,11 +514,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,
@@ -1142,48 +1174,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;
+		}
+
+		// TODO: Convert to DEBUG2
+		elog(LOG, "succeeded on %d password", j);
+
+		pg_hmac_free(ctx);
+
+		/* 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];
+
+		/* 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);
+
+		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;
+		}
 	}
 
-	pg_hmac_free(ctx);
-
-	/* 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];
-
-	/* 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);
-
-	if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0)
-		return false;
-
-	return true;
+	return false;
 }
 
 /*
@@ -1409,7 +1455,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 81dabb9c27..7cc9b13645 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -57,8 +57,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);
 
 
 /*----------------------------------------------------------------
@@ -789,8 +788,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);
 
@@ -798,17 +798,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);
-	}
-	else
-		result = STATUS_ERROR;
+		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);
+	}
 
-	if (shadow_pass)
-		pfree(shadow_pass);
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -823,54 +828,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);
+		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_password() returned error, return error, even if the
+	 * If get_role_passwords() returned error, return error, even if the
 	 * authentication succeeded.
 	 */
-	if (!shadow_pass)
+	if (!passwords)
 	{
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
@@ -883,11 +915,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))
@@ -903,12 +936,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 ef496a0bea..3b47af1269 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,20 +27,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;
+	Datum		second_datum;
+	bool		vuntil_isnull;
+	bool		second_vuntil_isnull;
+	bool		password_isnull;
+	bool		second_password_isnull;
 	char	   *shadow_pass;
+	char	   *second_shadow_pass;
+
+	*num_passwords = 0;
 
 	/* Get role info from pg_authid */
 	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
@@ -52,34 +61,73 @@ 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;
 }
 
 /*
@@ -113,7 +161,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);
@@ -134,13 +182,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'");
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index ddcd27469a..966eb4e627 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -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 7a1b1ed0a0..12c5c9602b 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 310bc36517..07d9cc3990 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.41.0

v3-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchapplication/x-patch; name=v3-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchDownload
From 6e272f44b48ce9749468f6289b23d0f7569f7862 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:54:11 -0700
Subject: [PATCH v3 3/3] Added SQL support for ALTER ROLE to manage two
 passwords

---
 src/backend/commands/user.c                   | 252 +++++++++++++++++-
 src/backend/parser/gram.y                     |  53 +++-
 .../regress/expected/password_rollover.out    | 140 ++++++++++
 src/test/regress/parallel_schedule            |   5 +
 src/test/regress/sql/password_rollover.sql    | 107 ++++++++
 5 files changed, 544 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 9ad02e4092..4721185e71 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -721,11 +721,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;
@@ -735,10 +740,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."));
@@ -750,9 +763,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)
 		{
@@ -809,6 +908,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)
@@ -822,6 +933,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);
@@ -830,8 +943,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.
@@ -867,7 +1002,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"),
@@ -875,7 +1010,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"),
@@ -934,15 +1069,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
@@ -1008,6 +1170,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)
@@ -1034,17 +1210,69 @@ 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;
+	}
+
 	/* 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)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e56cbe77cb..6447ac4056 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -361,7 +361,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
@@ -1168,6 +1169,7 @@ OptRoleList:
 
 AlterOptRoleList:
 			AlterOptRoleList AlterOptRoleElem		{ $$ = lappend($1, $2); }
+			| AlterOptRoleList AlterOnlyOptRoleElem	{ $$ = lappend($1, $2); }
 			| /* EMPTY */							{ $$ = NIL; }
 		;
 
@@ -1263,6 +1265,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..bad6d01b61
--- /dev/null
+++ b/src/test/regress/expected/password_rollover.out
@@ -0,0 +1,140 @@
+--
+-- Tests for password rollovers
+--
+SET password_encryption = 'md5';
+-- Create a user, 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 user 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 user 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 USER 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 USER 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';
+ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+-- the rolsecondpassword field should now contain a SCRAM secret
+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 | md5cc8c5dac5560a2fead71cfba4625a2c7 | Sat Jan 01 00:00:00 2022 PST | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> | infinity
+(1 row)
+
+-- Drop the less secure, md5, password
+ALTER USER regress_password_rollover1 DROP FIRST PASSWORD;
+-- 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)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4df9d8503b..5efad7f3ad 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..73a42f97ab
--- /dev/null
+++ b/src/test/regress/sql/password_rollover.sql
@@ -0,0 +1,107 @@
+--
+-- Tests for password rollovers
+--
+
+SET password_encryption = 'md5';
+
+-- Create a user, 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 user 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 user 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 USER 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 USER 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';
+
+ALTER USER regress_password_rollover1 ADD SECOND PASSWORD 'p7'
+    SECOND PASSWORD VALID UNTIL 'Infinity';
+
+-- the rolsecondpassword field should now contain a SCRAM secret
+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%';
+
+-- Drop the less secure, md5, password
+ALTER USER regress_password_rollover1 DROP FIRST PASSWORD;
+
+-- 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%';
-- 
2.41.0

#29Gurjeet Singh
gurjeet@singh.im
In reply to: Gurjeet Singh (#28)
11 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <gurjeet@singh.im> 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]password_rollover_v4 (910f81be54) https://github.com/gurjeet/postgres/commits/password_rollover_v4 on my private Git repo shows a clean run [1]password_rollover_v4 (910f81be54) https://github.com/gurjeet/postgres/commits/password_rollover_v4.

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
https://github.com/gurjeet/postgres/commits/password_rollover_v4

[2]: https://cirrus-ci.com/build/4675613999497216

Best regards,
Gurjeet
http://Gurje.et

Attachments:

v4-0001-Add-new-columns-to-pg_authid.patchapplication/octet-stream; name=v4-0001-Add-new-columns-to-pg_authid.patchDownload
From bc7c35e53e421157c9425c198bc2557ad118a650 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:36:05 -0700
Subject: [PATCH v4 01/11] 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 | 45 ++++++++++++++++++++-----------
 src/include/catalog/pg_authid.h   |  2 ++
 3 files changed, 36 insertions(+), 15 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index ce77a055e5..0afaf74865 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -452,9 +452,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 6b4a0aaaad..b326e48376 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -23,76 +23,91 @@
   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 => '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 0e7ddc56ea..2a27ae3e10 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.41.0

v4-0002-Update-password-verification-infrastructure-to-ha.patchapplication/octet-stream; name=v4-0002-Update-password-verification-infrastructure-to-ha.patchDownload
From 5bc11f3f617ee4ffe0b3c7e45edcd92aacdbf01d Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:48:11 -0700
Subject: [PATCH v4 02/11] 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 | 221 ++++++++++++++++++++-------------
 src/backend/libpq/auth.c       | 114 +++++++++++------
 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(+), 171 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 0afaf74865..9ad02e4092 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"
@@ -125,6 +127,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
@@ -153,8 +235,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;
@@ -442,11 +524,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
@@ -772,7 +859,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() ||
@@ -931,9 +1018,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 c535bc5383..a20aba8777 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 118d15b1a1..cd52e962ec 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -109,7 +109,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);
@@ -132,6 +132,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;
@@ -145,10 +151,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;
@@ -231,17 +243,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;
@@ -270,49 +285,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;
 	}
 
 	/*
@@ -323,10 +343,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;
 	}
 
@@ -469,12 +492,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;
@@ -491,11 +515,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,
@@ -1142,48 +1175,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;
+		}
+
+		// TODO: Convert to DEBUG2
+		elog(LOG, "succeeded on %d password", j);
+
+		pg_hmac_free(ctx);
+
+		/* 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];
+
+		/* 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);
+
+		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;
+		}
 	}
 
-	pg_hmac_free(ctx);
-
-	/* 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];
-
-	/* 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);
-
-	if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0)
-		return false;
-
-	return true;
+	return false;
 }
 
 /*
@@ -1409,7 +1456,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 81dabb9c27..7cc9b13645 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -57,8 +57,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);
 
 
 /*----------------------------------------------------------------
@@ -789,8 +788,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);
 
@@ -798,17 +798,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);
-	}
-	else
-		result = STATUS_ERROR;
+		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);
+	}
 
-	if (shadow_pass)
-		pfree(shadow_pass);
 	pfree(passwd);
 
 	if (result == STATUS_OK)
@@ -823,54 +828,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);
+		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_password() returned error, return error, even if the
+	 * If get_role_passwords() returned error, return error, even if the
 	 * authentication succeeded.
 	 */
-	if (!shadow_pass)
+	if (!passwords)
 	{
 		Assert(auth_result != STATUS_OK);
 		return STATUS_ERROR;
@@ -883,11 +915,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))
@@ -903,12 +936,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 ef496a0bea..eaea00e535 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-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -27,20 +27,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));
@@ -52,34 +61,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;
 }
 
 /*
@@ -113,7 +159,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);
@@ -134,13 +180,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'");
@@ -158,7 +204,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.
  *
@@ -213,7 +259,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 ef997ef684..a204bfd921 100644
--- a/src/common/scram-common.c
+++ b/src/common/scram-common.c
@@ -185,7 +185,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 ddcd27469a..5fc0841460 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 7a1b1ed0a0..12c5c9602b 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 310bc36517..07d9cc3990 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.41.0

v4-0005-Update-system-views-pg_roles-and-pg_shadow.patchapplication/octet-stream; name=v4-0005-Update-system-views-pg_roles-and-pg_shadow.patchDownload
From 443eed143edcaf7a770dddbae1e254f374a21dbf Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 21:17:12 -0700
Subject: [PATCH v4 05/11] 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 2b35c2f91b..28776fc268 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -2662,6 +2662,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>
@@ -3493,6 +3512,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 fcb14976c0..ba8a2393b2 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 2c60400ade..6b87e70766 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1486,6 +1486,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
@@ -1729,6 +1731,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.41.0

v4-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patchapplication/octet-stream; name=v4-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patchDownload
From 57d72ea1e40863b26abcdaae3635ec2bb586a4d9 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 13:49:03 -0700
Subject: [PATCH v4 04/11] Updated pg_dumpall to support exporting a role's
 second password

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

diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e2a9733d34..32688439c2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -755,6 +755,8 @@ dumpRoles(PGconn *conn)
 				i_rolconnlimit,
 				i_rolpassword,
 				i_rolvaliduntil,
+				i_rolsecondpassword,
+				i_rolsecondvaliduntil,
 				i_rolreplication,
 				i_rolbypassrls,
 				i_rolcomment,
@@ -762,12 +764,28 @@ dumpRoles(PGconn *conn)
 	int			i;
 
 	/* 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, "
 						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
 						  "rolname = current_user AS is_current_user "
 						  "FROM %s "
@@ -777,8 +795,10 @@ dumpRoles(PGconn *conn)
 		printfPQExpBuffer(buf,
 						  "SELECT oid, rolname, rolsuper, rolinherit, "
 						  "rolcreaterole, rolcreatedb, "
-						  "rolcanlogin, rolconnlimit, rolpassword, "
-						  "rolvaliduntil, rolreplication, rolbypassrls, "
+						  "rolcanlogin, rolconnlimit, "
+						  "rolpassword, rolvaliduntil, "
+						  "null as rolsecondpassword, null as rolsecodnvaliduntil, "
+						  "rolreplication, rolbypassrls, "
 						  "pg_catalog.shobj_description(oid, '%s') as rolcomment, "
 						  "rolname = current_user AS is_current_user "
 						  "FROM %s "
@@ -787,8 +807,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, '%s') as rolcomment, "
 						  "rolname = current_user AS is_current_user "
@@ -807,6 +829,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");
@@ -895,12 +919,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.41.0

v4-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchapplication/octet-stream; name=v4-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchDownload
From cd811b0fb1318fc47eeba7aa0beb4538bd1a966e Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 11:54:11 -0700
Subject: [PATCH v4 03/11] 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 9ad02e4092..01bcb7e7f2 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -721,11 +721,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;
@@ -735,10 +740,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."));
@@ -750,9 +763,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)
 		{
@@ -809,6 +908,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)
@@ -822,6 +933,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);
@@ -830,8 +943,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.
@@ -867,7 +1002,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"),
@@ -875,7 +1010,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"),
@@ -934,15 +1069,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
@@ -1008,6 +1170,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)
@@ -1034,17 +1210,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)
 	{
@@ -1559,6 +1871,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 e56cbe77cb..6447ac4056 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -361,7 +361,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
@@ -1168,6 +1169,7 @@ OptRoleList:
 
 AlterOptRoleList:
 			AlterOptRoleList AlterOptRoleElem		{ $$ = lappend($1, $2); }
+			| AlterOptRoleList AlterOnlyOptRoleElem	{ $$ = lappend($1, $2); }
 			| /* EMPTY */							{ $$ = NIL; }
 		;
 
@@ -1263,6 +1265,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 4df9d8503b..5efad7f3ad 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.41.0

v4-0008-Added-documentation-for-ALTER-ROLE-command.patchapplication/octet-stream; name=v4-0008-Added-documentation-for-ALTER-ROLE-command.patchDownload
From 84ed09c72c72fac5d1492454a82e146ecacb01d5 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Tue, 10 Oct 2023 01:14:49 -0700
Subject: [PATCH v4 08/11] 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 ab1ee45d54..459ea82a12 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>
 
@@ -126,6 +129,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">
@@ -189,6 +200,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>
@@ -335,6 +374,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.41.0

v4-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patchapplication/octet-stream; name=v4-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patchDownload
From 0c625694b480224acdffb15793a398bed6137c10 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Tue, 10 Oct 2023 02:07:57 -0700
Subject: [PATCH v4 09/11] 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 891860886a..873eaaccbc 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -662,4 +662,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.41.0

v4-0007-Updated-psql-s-describe-roles-meta-command.patchapplication/octet-stream; name=v4-0007-Updated-psql-s-describe-roles-meta-command.patchDownload
From 739c33fd9b0c7e06a8be5b77ee425c186bf33a26 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 21:41:51 -0700
Subject: [PATCH v4 07/11] 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 bac94a338c..485654dd7c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3675,7 +3675,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.41.0

v4-0006-Updated-pg_authid-catalog-documentation.patchapplication/octet-stream; name=v4-0006-Updated-pg_authid-catalog-documentation.patchDownload
From 5022dca81585c1fcc53ce601ad863b933e48cb5c Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Mon, 9 Oct 2023 21:38:06 -0700
Subject: [PATCH v4 06/11] 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 e09adb45e4..4bd19d1f26 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>
-   For an MD5 encrypted password, <structfield>rolpassword</structfield>
+   <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, 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.41.0

v4-0010-pgindent-run.patchapplication/octet-stream; name=v4-0010-pgindent-run.patchDownload
From ad73fb17dcb1d183f646aaa3d50665484ce573fd Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Tue, 10 Oct 2023 02:46:48 -0700
Subject: [PATCH v4 10/11] pgindent run

---
 src/backend/commands/user.c      | 170 ++++++++++++++++---------------
 src/backend/libpq/auth-scram.c   |  75 ++++++++------
 src/backend/libpq/auth.c         |  33 +++---
 src/backend/libpq/crypt.c        |  20 ++--
 src/include/catalog/pg_authid.h  |   3 +-
 src/include/utils/wait_event.h   |   2 +-
 src/tools/pgindent/typedefs.list |   7 +-
 7 files changed, 166 insertions(+), 144 deletions(-)

diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 01bcb7e7f2..829a74205b 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -139,25 +139,29 @@ static bool
 get_salt(char *rolename, char **salt, const char **logdetail)
 {
 	char	  **current_secrets;
-	int			i, num_secrets;
-	char	   *salt1, *salt2 = NULL;
+	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 */
+		*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 */
+		*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 */
+		*salt = NULL;			/* No existing passwords, allow salt to be
+								 * generated */
 		return true;
 	}
 
@@ -166,34 +170,36 @@ get_salt(char *rolename, char **salt, const char **logdetail)
 		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 */
+			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];
+			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 (!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 (salt2 != NULL)
+			{
+				if (strcmp(salt1, salt2))
 				{
-					if (strcmp(salt1, salt2))
-					{
-						*logdetail = psprintf(_("inconsistent salts, clearing password")); // TODO: Better message
-						*salt = NULL;
-						return false;
-					}
+					*logdetail = psprintf(_("inconsistent salts, clearing password"));
+			//TODO: Better message
+						* salt = NULL;
+					return false;
 				}
-				else
-					salt2 = salt1;
+			}
+			else
+				salt2 = salt1;
 		}
 	}
 
@@ -524,12 +530,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 		}
 		else
 		{
-			char *salt;
+			char	   *salt;
 
 			if (!get_salt(stmt->role, &salt, &logdetail))
 				ereport(ERROR,
 						(errmsg("could not get a valid salt for password"),
-						errdetail("%s", logdetail)));
+						 errdetail("%s", logdetail)));
 
 			/* Encrypt the password to the requested format. */
 			shadow_pass = encrypt_password(Password_encryption, salt, password);
@@ -721,13 +727,15 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	ListCell   *option;
 	char	   *rolename;
 	char	   *password = NULL;	/* user password */
-	char	   *second_password = NULL;	/* user's second password */
+	char	   *second_password = NULL; /* user's second password */
 	int			connlimit = -1; /* maximum connections allowed */
 	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 */
+	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;
@@ -816,6 +824,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 				errorConflictingDefElem(defel, pstate);
 			dsecondpassword = defel;
 			addSecondPassword = true;
+
 			/*
 			 * Adding and dropping passwords in the same command is not
 			 * supported.
@@ -825,7 +834,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		}
 		else if (strcmp(defel->defname, "drop-password") == 0)
 		{
-			char *which = strVal(defel->arg);
+			char	   *which = strVal(defel->arg);
 
 			if (strcmp(which, "first") == 0)
 			{
@@ -948,15 +957,15 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	 * 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.
+	 * 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 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))
@@ -1073,17 +1082,17 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	if (dsecondValidUntil)
 	{
 		secondValidUntil_datum = DirectFunctionCall3(timestamptz_in,
-											   CStringGetDatum(secondValidUntil),
-											   ObjectIdGetDatum(InvalidOid),
-											   Int32GetDatum(-1));
+													 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);
+												 Anum_pg_authid_rolsecondvaliduntil,
+												 &secondValidUntil_null);
 	}
 
 	/*
@@ -1172,7 +1181,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 
 		if (addFirstPassword)
 		{
-			bool	firstPassword_null;
+			bool		firstPassword_null;
 
 			SysCacheGetAttr(AUTHNAME, tuple,
 							Anum_pg_authid_rolpassword,
@@ -1181,7 +1190,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 			if (!firstPassword_null)
 				ereport(ERROR,
 						(errmsg("first password is already in use"),
-						errdetail("Use ALTER ROLE DROP FIRST PASSWORD.")));
+						 errdetail("Use ALTER ROLE DROP FIRST PASSWORD.")));
 		}
 
 		/* Like in CREATE USER, don't allow an empty password. */
@@ -1199,8 +1208,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 			if (!get_salt(rolename, &salt, &logdetail))
 				ereport(ERROR,
 						(errcode(ERRCODE_INTERNAL_ERROR),
-						errmsg("could not get a valid salt for password"),
-						errdetail("%s", logdetail)));
+						 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, password);
@@ -1224,7 +1233,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 		if (!secondPassword_null)
 			ereport(ERROR,
 					(errmsg("second password is already in use"),
-					errdetail("Use ALTER ROLE DROP SECOND PASSWORD")));
+					 errdetail("Use ALTER ROLE DROP SECOND PASSWORD")));
 
 		/* Like in CREATE USER, don't allow an empty password. */
 		if (second_password[0] == '\0' ||
@@ -1241,8 +1250,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 			if (!get_salt(rolename, &salt, &logdetail))
 				ereport(ERROR,
 						(errcode(ERRCODE_INTERNAL_ERROR),
-						errmsg("could not get a valid salt for password"),
-						errdetail("%s", logdetail)));
+						 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);
@@ -1253,28 +1262,29 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 	}
 
 	/*
-	 * 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.
+	 * 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 */
+		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);
+											  Anum_pg_authid_rolpassword,
+											  &firstPassword_null);
 		secondPassword_datum = SysCacheGetAttr(AUTHNAME, tuple,
-												Anum_pg_authid_rolsecondpassword,
-												&secondPassword_null);
+											   Anum_pg_authid_rolsecondpassword,
+											   &secondPassword_null);
 		if (!firstPassword_null)
 		{
 			roleFirstPassword = TextDatumGetCString(firstPassword_datum);
@@ -1287,52 +1297,52 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
 			roleSecondPasswordType = get_password_type(roleSecondPassword);
 		}
 
-			/* if the user requested setting the first password ... */
+		/* if the user requested setting the first password ... */
 		if ((overwriteFirstPassword || addFirstPassword) &&
-			/* and we have decided to honor their request */
+		/* 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 */
+		/* 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 */
+		/* 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.")));
+						 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.")));
+						 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.")));
+						 errdetail("The new password must also use plaintext.")));
 		}
 
-			/* if the user requested setting the second password ... */
+		/* if the user requested setting the second password ... */
 		if (addSecondPassword &&
-			/* and we have decided to honor their request */
+		/* 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 */
+		/* 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 */
+		/* 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.")));
+						 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.")));
+						 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.")));
+						 errdetail("The new password must also use plaintext.")));
 		}
 	}
 
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index cd52e962ec..e7a2c773ce 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -136,7 +136,7 @@ typedef struct
 {
 	uint8		StoredKey[SCRAM_MAX_KEY_LEN];
 	uint8		ServerKey[SCRAM_MAX_KEY_LEN];
-} scram_secret;
+}			scram_secret;
 
 typedef struct
 {
@@ -152,15 +152,15 @@ typedef struct
 	int			key_length;
 
 	/*
-	 * The salt and iterations must be the same for all
-	 * secrets since they are sent as part of the initial message
+	 * 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 */
 	/* Array of possible secrets */
 	scram_secret *secrets;
 	int			num_secrets;
-	int			chosen_secret; /* secret chosen during final client message */
+	int			chosen_secret;	/* secret chosen during final client message */
 
 	/* Fields of the first message from client */
 	char		cbind_flag;
@@ -255,8 +255,8 @@ scram_init(Port *port, const char *selected_mech, const char **secrets, const in
 	scram_state *state;
 	bool		got_secret = false;
 	int			i;
-	int	iterations;
-	char *salt = NULL;			/* base64-encoded */
+	int			iterations;
+	char	   *salt = NULL;	/* base64-encoded */
 
 	state = (scram_state *) palloc0(sizeof(scram_state));
 	state->port = port;
@@ -303,12 +303,16 @@ scram_init(Port *port, const char *selected_mech, const char **secrets, const in
 				{
 					if (salt)
 					{
-						/* The stored iterations and salt must match or we cannot proceed, allow failure via mock */
+						/*
+						 * 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 */
+													 state->port->user_name)));
+							got_secret = false; /* fail and allow mock
+												 * creditials to be created */
 							pfree(state->secrets);
 							state->num_secrets = 0;
 							break;
@@ -318,15 +322,16 @@ scram_init(Port *port, const char *selected_mech, const char **secrets, const in
 					{
 						salt = state->salt;
 						iterations = state->iterations;
-						got_secret = true; /* We got at least one good SCRAM secret */
+						got_secret = true;	/* We got at least one good SCRAM
+											 * secret */
 					}
 				}
 				else
 				{
 					/*
-					* The password looked like a SCRAM secret, but could not be
-					* parsed.
-					*/
+					 * 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)));
@@ -527,7 +532,7 @@ pg_be_scram_build_secret(const char *password, const char *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")));
+					 errmsg("could not decode SCRAM salt")));
 	}
 
 	result = scram_build_secret(PG_SHA256, SCRAM_SHA_256_KEY_LEN,
@@ -1176,8 +1181,10 @@ verify_client_proof(scram_state *state)
 	uint8		ClientKey[SCRAM_MAX_KEY_LEN];
 	uint8		client_StoredKey[SCRAM_MAX_KEY_LEN];
 	pg_hmac_ctx *ctx;
-	int			i, j;
+	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
@@ -1186,29 +1193,30 @@ verify_client_proof(scram_state *state)
 	for (j = 0; j < state->num_secrets; j++)
 	{
 		ctx = pg_hmac_create(state->hash_type);
-		elog(LOG, "Trying to verify password %d", j); // TODO: Convert to DEBUG2
+		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)
+			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
+			/* TODO: Convert to DEBUG2 */
 			elog(LOG, "could not calculate client signature for secret %d", j);
 			pg_hmac_free(ctx);
 			continue;
 		}
 
-		// TODO: Convert to DEBUG2
+		/* TODO: Convert to DEBUG2 */
 		elog(LOG, "succeeded on %d password", j);
 
 		pg_hmac_free(ctx);
@@ -1222,8 +1230,9 @@ verify_client_proof(scram_state *state)
 					client_StoredKey, &errstr) < 0)
 			elog(ERROR, "could not hash stored key: %s", errstr);
 
-		if (memcmp(client_StoredKey, state->secrets[j].StoredKey, state->key_length) == 0) {
-			// TODO: Convert to DEBUG2
+		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;
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 7cc9b13645..04022eb288 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -789,8 +789,9 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 {
 	char	   *passwd;
 	int			result = STATUS_ERROR;
-	int			i, num_passwords;
-	char	   **passwords;
+	int			i,
+				num_passwords;
+	char	  **passwords;
 
 	sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
 
@@ -806,7 +807,8 @@ CheckPasswordAuth(Port *port, const char **logdetail)
 			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 */
+				break;			/* Found a matching password, no need to try
+								 * any others */
 		}
 		for (i = 0; i < num_passwords; i++)
 			pfree(passwords[i]);
@@ -830,9 +832,10 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 {
 	bool		scram_pw_avail = false;
 	int			auth_result = STATUS_ERROR;
-	int			i, num_passwords;
+	int			i,
+				num_passwords;
 	char	  **passwords;
-	PasswordType	pwtype;
+	PasswordType pwtype;
 
 	Assert(port->hba->auth_method == uaSCRAM ||
 		   port->hba->auth_method == uaMD5);
@@ -842,12 +845,12 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 
 	/*
 	 * 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.
+	 * 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 (!passwords)
 		pwtype = Password_encryption;
@@ -855,8 +858,8 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 	/*
 	 * 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 there's a SCRAM password available then we'll do SCRAM, otherwise we
-	 * will fall back to trying to use MD5.
+	 * 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
@@ -885,7 +888,7 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 			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);
+										logdetail);
 
 		for (i = 0; i < num_passwords; i++)
 		{
@@ -893,7 +896,7 @@ CheckPWChallengeAuth(Port *port, const char **logdetail)
 				pfree(passwords[i]);
 			else
 				ereport(DEBUG2,
-					(errmsg("Password %d was null", i)));
+						(errmsg("Password %d was null", i)));
 		}
 		pfree(passwords);
 	}
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index eaea00e535..d56f3bc399 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -33,7 +33,7 @@
  * for the postmaster log, in *logdetail.  The error reason should *not* be
  * sent to the client, to avoid giving away user information!
  */
-char **
+char	  **
 get_role_passwords(const char *role, const char **logdetail, int *num_passwords)
 {
 	TimestampTz vuntil = 0;
@@ -64,8 +64,8 @@ get_role_passwords(const char *role, const char **logdetail, int *num_passwords)
 							Anum_pg_authid_rolpassword,
 							&password_isnull);
 	second_datum = SysCacheGetAttr(AUTHNAME, roleTup,
-									Anum_pg_authid_rolsecondpassword,
-									&second_password_isnull);
+								   Anum_pg_authid_rolsecondpassword,
+								   &second_password_isnull);
 	if (password_isnull && second_password_isnull)
 	{
 		ReleaseSysCache(roleTup);
@@ -82,8 +82,8 @@ get_role_passwords(const char *role, const char **logdetail, int *num_passwords)
 	datum = SysCacheGetAttr(AUTHNAME, roleTup,
 							Anum_pg_authid_rolvaliduntil, &vuntil_isnull);
 	second_datum = SysCacheGetAttr(AUTHNAME, roleTup,
-							Anum_pg_authid_rolsecondvaliduntil,
-							&second_vuntil_isnull);
+								   Anum_pg_authid_rolsecondvaliduntil,
+								   &second_vuntil_isnull);
 	if (!vuntil_isnull)
 		vuntil = DatumGetTimestampTz(datum);
 	if (!second_vuntil_isnull)
@@ -96,14 +96,14 @@ get_role_passwords(const char *role, const char **logdetail, int *num_passwords)
 	 */
 	current_ts = GetCurrentTimestamp();
 	*num_passwords = (!password_isnull &&
-						(vuntil_isnull || vuntil >= current_ts))
-					+ (!second_password_isnull &&
-						(second_vuntil_isnull || second_vuntil >= current_ts));
+					  (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));
+		int			i = 0;
+		char	  **passwords = palloc(sizeof(char *) * (*num_passwords));
 
 		if (!password_isnull && (vuntil_isnull || vuntil >= current_ts))
 		{
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 2a27ae3e10..b5107950c5 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -46,7 +46,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
 	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 */
+	timestamptz rolsecondvaliduntil;	/* second password expiration time, if
+										 * any */
 #endif
 } FormData_pg_authid;
 
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 009b03a520..5a7817cb32 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -57,7 +57,7 @@ typedef enum
 {
 	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
 	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
-} WaitEventExtension;
+}			WaitEventExtension;
 
 extern void WaitEventExtensionShmemInit(void);
 extern Size WaitEventExtensionShmemSize(void);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8de90c4958..be8e472473 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1275,9 +1275,9 @@ JsonManifestWALRangeField
 JsonObjectAgg
 JsonObjectConstructor
 JsonOutput
-JsonParseExpr
 JsonParseContext
 JsonParseErrorType
+JsonParseExpr
 JsonPath
 JsonPathBool
 JsonPathExecContext
@@ -1340,6 +1340,7 @@ LINE
 LLVMAttributeRef
 LLVMBasicBlockRef
 LLVMBuilderRef
+LLVMContextRef
 LLVMErrorRef
 LLVMIntPredicate
 LLVMJITEventListenerRef
@@ -1913,7 +1914,6 @@ ParallelHashJoinBatch
 ParallelHashJoinBatchAccessor
 ParallelHashJoinState
 ParallelIndexScanDesc
-ParallelReadyList
 ParallelSlot
 ParallelSlotArray
 ParallelSlotResultHandler
@@ -2992,7 +2992,6 @@ WaitEvent
 WaitEventActivity
 WaitEventBufferPin
 WaitEventClient
-WaitEventExtension
 WaitEventExtensionCounterData
 WaitEventExtensionEntryById
 WaitEventExtensionEntryByName
@@ -3401,6 +3400,7 @@ indexed_tlist
 inet
 inetKEY
 inet_struct
+initRowMethod
 init_function
 inline_cte_walker_context
 inline_error_callback_arg
@@ -3868,7 +3868,6 @@ wchar2mb_with_len_converter
 wchar_t
 win32_deadchild_waitinfo
 wint_t
-worker_spi_state
 worker_state
 worktable
 wrap
-- 
2.41.0

v4-0011-Run-pgperltidy-on-files-changed-by-this-patchset.patchapplication/octet-stream; name=v4-0011-Run-pgperltidy-on-files-changed-by-this-patchset.patchDownload
From 29849a31e845ca263ba8e653b00a286ef05dccbf Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
Date: Tue, 10 Oct 2023 02:52:33 -0700
Subject: [PATCH v4 11/11] Run pgperltidy on files changed by this patchset

There many other perl files updated by pgperltidy, but I chose to not
include them in this patch, to avoid any confusion.
---
 src/test/authentication/t/001_password.pl | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 873eaaccbc..09dfecb7c8 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -699,8 +699,7 @@ $ENV{"PGPASSWORD"} = 'md5';
 test_conn(
 	$node,
 	'user=regress_password_rollover_md5',
-	'md5',
-	0,
+	'md5', 0,
 	log_like => [
 		qr/connection authenticated: identity="regress_password_rollover_md5" method=md5/
 	]);
@@ -708,8 +707,7 @@ $ENV{"PGPASSWORD"} = 'md5_2';
 test_conn(
 	$node,
 	'user=regress_password_rollover_md5',
-	'md5',
-	0,
+	'md5', 0,
 	log_like => [
 		qr/connection authenticated: identity="regress_password_rollover_md5" method=md5/
 	]);
-- 
2.41.0

#30vignesh C
vignesh21@gmail.com
In reply to: Gurjeet Singh (#29)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Tue, 10 Oct 2023 at 16:37, Gurjeet Singh <gurjeet@singh.im> wrote:

On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <gurjeet@singh.im> 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.

CFBot shows that the patch does not apply anymore as in [1]http://cfbot.cputube.org/patch_46_4432.log:
=== Applying patches on top of PostgreSQL commit ID
4d969b2f85e1fd00e860366f101fd3e3160aab41 ===
=== applying patch
./v4-0002-Update-password-verification-infrastructure-to-ha.patch
...
patching file src/backend/libpq/auth.c
Hunk #4 FAILED at 828.
Hunk #5 succeeded at 886 (offset -2 lines).
Hunk #6 succeeded at 907 (offset -2 lines).
1 out of 6 hunks FAILED -- saving rejects to file src/backend/libpq/auth.c.rej

Please post an updated version for the same.

[1]: http://cfbot.cputube.org/patch_46_4432.log

Regards,
Vignesh

#31vignesh C
vignesh21@gmail.com
In reply to: vignesh C (#30)
Re: [PoC/RFC] Multiple passwords, interval expirations

On Sat, 27 Jan 2024 at 07:18, vignesh C <vignesh21@gmail.com> wrote:

On Tue, 10 Oct 2023 at 16:37, Gurjeet Singh <gurjeet@singh.im> wrote:

On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <gurjeet@singh.im> 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.

CFBot shows that the patch does not apply anymore as in [1]:
=== Applying patches on top of PostgreSQL commit ID
4d969b2f85e1fd00e860366f101fd3e3160aab41 ===
=== applying patch
./v4-0002-Update-password-verification-infrastructure-to-ha.patch
...
patching file src/backend/libpq/auth.c
Hunk #4 FAILED at 828.
Hunk #5 succeeded at 886 (offset -2 lines).
Hunk #6 succeeded at 907 (offset -2 lines).
1 out of 6 hunks FAILED -- saving rejects to file src/backend/libpq/auth.c.rej

Please post an updated version for the same.

The patch which you submitted has been awaiting your attention for
quite some time now. As such, we have moved it to "Returned with
Feedback" and removed it from the reviewing queue. Depending on
timing, this may be reversible. Kindly address the feedback you have
received, and resubmit the patch to the next CommitFest.

Regards,
Vignesh

#32Kirill Reshke
reshkekirill@gmail.com
In reply to: Gurjeet Singh (#29)
9 attachment(s)
Re: [PoC/RFC] Multiple passwords, interval expirations

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 <gurjeet@singh.im> wrote:

On Mon, Oct 9, 2023 at 2:31 AM Gurjeet Singh <gurjeet@singh.im> 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:

v5-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchapplication/octet-stream; name=v5-0003-Added-SQL-support-for-ALTER-ROLE-to-manage-two-pa.patchDownload
From bc598e00a3d57ad1a1d6e56fc7b969b0ad2886d6 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0001-Add-new-columns-to-pg_authid.patchapplication/octet-stream; name=v5-0001-Add-new-columns-to-pg_authid.patchDownload
From b016e1dca81947d811425d2b43b050891bd80739 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patchapplication/octet-stream; name=v5-0004-Updated-pg_dumpall-to-support-exporting-a-role-s-.patchDownload
From 4e3603b98c689d88670d6cf454cbe988aa0134cc Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0002-Update-password-verification-infrastructure-to-ha.patchapplication/octet-stream; name=v5-0002-Update-password-verification-infrastructure-to-ha.patchDownload
From d62c9741d71f46a672d5675e0dbbe8d956d79242 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0005-Update-system-views-pg_roles-and-pg_shadow.patchapplication/octet-stream; name=v5-0005-Update-system-views-pg_roles-and-pg_shadow.patchDownload
From 8b18b5003ee6f77769c91952aa231802b5069513 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0007-Updated-psql-s-describe-roles-meta-command.patchapplication/octet-stream; name=v5-0007-Updated-psql-s-describe-roles-meta-command.patchDownload
From f20a487f6bab8bb1e0a6c42d5a9af8edb8d77486 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0006-Updated-pg_authid-catalog-documentation.patchapplication/octet-stream; name=v5-0006-Updated-pg_authid-catalog-documentation.patchDownload
From 470b5f2c369670af0c130194ffa47f7210b2e7c9 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0008-Added-documentation-for-ALTER-ROLE-command.patchapplication/octet-stream; name=v5-0008-Added-documentation-for-ALTER-ROLE-command.patchDownload
From 6686a7d98eb65237228aa9d790bb6e199ff9bca3 Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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

v5-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patchapplication/octet-stream; name=v5-0009-Added-TAP-tests-to-prove-that-a-role-can-use-two-.patchDownload
From ed6680897e5087fac2b1b8bd78d83ce8f5ce10ac Mon Sep 17 00:00:00 2001
From: Gurjeet Singh <gurjeet@singh.im>
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