</varlistentry>
<varlistentry>
- <term><literal>md5</></term>
+ <term><literal>scram</></term>
<listitem>
<para>
- Require the client to supply a double-MD5-hashed password for
- authentication.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 authentication to verify the user's
+ password. See <xref linkend="auth-password"> for details.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>scram</></term>
+ <term><literal>md5</></term>
<listitem>
<para>
- Perform SCRAM-SHA-256 authentication to verify the user's
- password.
- See <xref linkend="auth-password"> for details.
+ Perform SCRAM-SHA-256 or MD5 authentication to verify the
+ user's password. See <xref linkend="auth-password">
+ for details.
</para>
</listitem>
</varlistentry>
# Allow any user from hosts in the example.com domain to connect to
# any database if the user's password is correctly supplied.
#
-# Most users use SCRAM authentication, but some users use older clients
-# that don't support SCRAM authentication, and need to be able to log
-# in using MD5 authentication. Such users are put in the @md5users
-# group, everyone else must use SCRAM.
+# Require SCRAM authentication for most users, but make an exception
+# for user 'mike', who uses an older client that doesn't support SCRAM
+# authentication.
#
# TYPE DATABASE USER ADDRESS METHOD
-host all @md5users .example.com md5
+host all mike .example.com md5
host all all .example.com scram
# In the absence of preceding "host" lines, these two lines will
</para>
<para>
- In <literal>md5</>, the client sends a hash of a random challenge,
- generated by the server, and the password. It prevents password sniffing,
- but is less secure than <literal>scram</>, and provides no protection
- if an attacker manages to steal the password hash from the server.
- <literal>md5</> cannot be used with the <xref
- linkend="guc-db-user-namespace"> feature.
+ <literal>md5</> allows falling back to a less secure challenge-response
+ mechanism for those users with an MD5 hashed password.
+ The fallback mechanism also prevents password sniffing, but provides no
+ protection if an attacker manages to steal the password hash from the
+ server, and it cannot be used with the <xref
+ linkend="guc-db-user-namespace"> feature. For all other users,
+ <literal>md5</> works the same as <literal>scram</>.
</para>
<para>
* after the beginning of the exchange with verifier data.
*
* 'username' is the provided by the client. 'shadow_pass' is the role's
- * password verifier, from pg_authid.rolpassword. If 'doomed' is true, the
- * authentication must fail, as if an incorrect password was given.
- * 'shadow_pass' may be NULL, when 'doomed' is set.
+ * password verifier, from pg_authid.rolpassword. If 'shadow_pass' is NULL, we
+ * still perform an authentication exchange, but it will fail, as if an
+ * incorrect password was given.
*/
void *
-pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed)
+pg_be_scram_init(const char *username, const char *shadow_pass)
{
scram_state *state;
- int password_type;
+ bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
state->state = SCRAM_AUTH_INIT;
state->username = username;
/*
- * Perform sanity checks on the provided password after catalog lookup.
- * The authentication is bound to fail if the lookup itself failed or if
- * the password stored is MD5-encrypted. Authentication is possible for
- * users with a valid plain password though.
+ * Parse the stored password verifier.
*/
+ if (shadow_pass)
+ {
+ int password_type = get_password_type(shadow_pass);
- if (shadow_pass == NULL || doomed)
- password_type = -1;
- else
- password_type = get_password_type(shadow_pass);
+ if (password_type == PASSWORD_TYPE_SCRAM)
+ {
+ if (parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey))
+ got_verifier = true;
+ else
+ {
+ /*
+ * The password looked like a SCRAM verifier, but could not be
+ * parsed.
+ */
+ elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
+ got_verifier = false;
+ }
+ }
+ else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ {
+ /*
+ * The stored password is in plain format. Generate a fresh SCRAM
+ * verifier from it, and proceed with that.
+ */
+ char *verifier;
- if (password_type == PASSWORD_TYPE_SCRAM)
- {
- if (!parse_scram_verifier(shadow_pass, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey))
+ verifier = scram_build_verifier(username, shadow_pass, 0);
+
+ (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
+ state->StoredKey, state->ServerKey);
+ pfree(verifier);
+
+ got_verifier = true;
+ }
+ else
{
/*
- * The password looked like a SCRAM verifier, but could not be
- * parsed.
+ * The user doesn't have SCRAM verifier, nor could we generate
+ * one. (You cannot do SCRAM authentication with an MD5 hash.)
*/
- elog(LOG, "invalid SCRAM verifier for user \"%s\"", username);
- doomed = true;
+ state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
+ state->username);
+ got_verifier = false;
}
}
- else if (password_type == PASSWORD_TYPE_PLAINTEXT)
+ else
{
- char *verifier;
-
/*
- * The password provided is in plain format, in which case a fresh
- * SCRAM verifier can be generated and used for the rest of the
- * processing.
+ * The caller requested us to perform a dummy authentication. This is
+ * considered normal, since the caller requested it, so don't set log
+ * detail.
*/
- verifier = scram_build_verifier(username, shadow_pass, 0);
-
- (void) parse_scram_verifier(verifier, &state->salt, &state->iterations,
- state->StoredKey, state->ServerKey);
- pfree(verifier);
+ got_verifier = false;
}
- else
- doomed = true;
- if (doomed)
+ /*
+ * If the user did not have a valid SCRAM verifier, we still go through
+ * the motions with a mock one, and fail as if the client supplied an
+ * incorrect password. This is to avoid revealing information to an
+ * attacker.
+ */
+ if (!got_verifier)
{
- /*
- * We don't have a valid SCRAM verifier, nor could we generate one, or
- * the caller requested us to perform a dummy authentication.
- *
- * The authentication is bound to fail, but to avoid revealing
- * information to the attacker, go through the motions with a fake
- * SCRAM verifier, and fail as if the password was incorrect.
- */
- state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
mock_scram_verifier(username, &state->salt, &state->iterations,
state->StoredKey, state->ServerKey);
+ state->doomed = true;
}
- state->doomed = doomed;
return state;
}
#include <sys/select.h>
#endif
+#include "commands/user.h"
#include "common/ip.h"
#include "common/md5.h"
#include "libpq/auth.h"
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication methods (password, md5, and scram)
*----------------------------------------------------------------
*/
-static int CheckMD5Auth(Port *port, char **logdetail);
+static int CheckPasswordAuth(Port *port, char **logdetail);
+static int CheckPWChallengeAuth(Port *port, char **logdetail);
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
- */
+static int CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail);
+static int CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail);
-static int CheckPasswordAuth(Port *port, char **logdetail);
/*----------------------------------------------------------------
* Ident authentication
static int PerformRadiusTransaction(char *server, char *secret, char *portstr, char *identifier, char *user_name, char *passwd);
-/*----------------------------------------------------------------
- * SASL authentication
- *----------------------------------------------------------------
- */
-static int CheckSASLAuth(Port *port, char **logdetail);
-
/*
* Maximum accepted size of GSS and SSPI authentication tokens.
*
break;
case uaPassword:
case uaMD5:
- case uaSASL:
+ case uaSCRAM:
errstr = gettext_noop("password authentication failed for user \"%s\"");
/* We use it to indicate if a .pgpass password failed. */
errcode_return = ERRCODE_INVALID_PASSWORD;
break;
case uaMD5:
- status = CheckMD5Auth(port, &logdetail);
+ case uaSCRAM:
+ status = CheckPWChallengeAuth(port, &logdetail);
break;
case uaPassword:
status = CheckPasswordAuth(port, &logdetail);
break;
- case uaSASL:
- status = CheckSASLAuth(port, &logdetail);
- break;
-
case uaPAM:
#ifdef USE_PAM
status = CheckPAMAuth(port, port->user_name, "");
/*----------------------------------------------------------------
- * MD5 authentication
+ * Password-based authentication mechanisms
*----------------------------------------------------------------
*/
+/*
+ * Plaintext password authentication.
+ */
static int
-CheckMD5Auth(Port *port, char **logdetail)
+CheckPasswordAuth(Port *port, char **logdetail)
{
- char md5Salt[4]; /* Password salt */
char *passwd;
- char *shadow_pass;
int result;
+ char *shadow_pass;
- if (Db_user_namespace)
- ereport(FATAL,
- (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
- errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
-
- /* include the salt to use for computing the response */
- if (!pg_backend_random(md5Salt, 4))
- {
- ereport(LOG,
- (errmsg("could not generate random MD5 salt")));
- return STATUS_ERROR;
- }
-
- sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
+ sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
- if (result == STATUS_OK)
- result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
- md5Salt, 4, logdetail);
+ 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;
if (shadow_pass)
pfree(shadow_pass);
return result;
}
-/*----------------------------------------------------------------
- * Plaintext password authentication
- *----------------------------------------------------------------
+/*
+ * MD5 and SCRAM authentication.
*/
+static int
+CheckPWChallengeAuth(Port *port, char **logdetail)
+{
+ int auth_result;
+ char *shadow_pass;
+ 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);
+
+ /*
+ * If the user does not exist, or has no password, 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' authentication based on current password_encryption setting.
+ * The idea is that most genuine users probably have a password of that
+ * type, if we pretend that this user had a password of that type, too, it
+ * "blends in" best.
+ *
+ * If the user had a password, but it was expired, we'll use the details
+ * of the expired password for the authentication, but report it as
+ * failure to the client even if correct password was given.
+ */
+ if (!shadow_pass)
+ pwtype = Password_encryption;
+ else
+ pwtype = get_password_type(shadow_pass);
+
+ /*
+ * If 'md5' authentication is allowed, decide whether to perform 'md5' or
+ * 'scram' 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
+ * verifier, we must do SCRAM authentication. If it's stored in
+ * plaintext, we could do either one, so we opt for the more secure
+ * mechanism, SCRAM.
+ *
+ * If MD5 authentication is not allowed, always use SCRAM. If the user
+ * had an MD5 password, CheckSCRAMAuth() will fail.
+ */
+ if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)
+ {
+ auth_result = CheckMD5Auth(port, shadow_pass, logdetail);
+ }
+ else
+ {
+ auth_result = CheckSCRAMAuth(port, shadow_pass, logdetail);
+ }
+
+ if (shadow_pass)
+ pfree(shadow_pass);
+
+ /*
+ * If get_role_password() returned error, return error, even if the
+ * authentication succeeded.
+ */
+ if (!shadow_pass)
+ {
+ Assert(auth_result != STATUS_OK);
+ return STATUS_ERROR;
+ }
+ return auth_result;
+}
static int
-CheckPasswordAuth(Port *port, char **logdetail)
+CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail)
{
+ char md5Salt[4]; /* Password salt */
char *passwd;
int result;
- char *shadow_pass;
- sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
+ if (Db_user_namespace)
+ ereport(FATAL,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
+
+ /* include the salt to use for computing the response */
+ if (!pg_backend_random(md5Salt, 4))
+ {
+ ereport(LOG,
+ (errmsg("could not generate random MD5 salt")));
+ return STATUS_ERROR;
+ }
+
+ sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
- result = get_role_password(port->user_name, &shadow_pass, logdetail);
- if (result == STATUS_OK)
- result = plain_crypt_verify(port->user_name, shadow_pass, passwd,
- logdetail);
-
if (shadow_pass)
- pfree(shadow_pass);
+ result = md5_crypt_verify(port->user_name, shadow_pass, passwd,
+ md5Salt, 4, logdetail);
+ else
+ result = STATUS_ERROR;
+
pfree(passwd);
return result;
}
-/*----------------------------------------------------------------
- * SASL authentication system
- *----------------------------------------------------------------
- */
static int
-CheckSASLAuth(Port *port, char **logdetail)
+CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
{
int mtype;
StringInfoData buf;
char *output = NULL;
int outputlen = 0;
int result;
- char *shadow_pass;
- bool doomed = false;
/*
* SASL auth is not supported for protocol versions before 3, because it
* This is because we don't want to reveal to an attacker what usernames
* are valid, nor which users have a valid password.
*/
- if (get_role_password(port->user_name, &shadow_pass, logdetail) != STATUS_OK)
- doomed = true;
/* Initialize the status tracker for message exchanges */
- scram_opaq = pg_be_scram_init(port->user_name, shadow_pass, doomed);
+ scram_opaq = pg_be_scram_init(port->user_name, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
*/
result = pg_be_scram_exchange(scram_opaq, buf.data, buf.len,
&output, &outputlen,
- doomed ? NULL : logdetail);
+ logdetail);
/* input buffer no longer used */
pfree(buf.data);
/*
* Fetch stored password for a user, for authentication.
*
- * Returns STATUS_OK on success. On error, returns STATUS_ERROR, 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!
- *
- * If the password is expired, it is still returned in *shadow_pass, but the
- * return code is STATUS_ERROR. On other errors, *shadow_pass is set to
- * NULL.
+ * 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!
*/
-int
-get_role_password(const char *role, char **shadow_pass, char **logdetail)
+char *
+get_role_password(const char *role, char **logdetail)
{
- int retval = STATUS_ERROR;
TimestampTz vuntil = 0;
HeapTuple roleTup;
Datum datum;
bool isnull;
-
- *shadow_pass = NULL;
+ char *shadow_pass;
/* Get role info from pg_authid */
roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
{
*logdetail = psprintf(_("Role \"%s\" does not exist."),
role);
- return STATUS_ERROR; /* no such user */
+ return NULL; /* no such user */
}
datum = SysCacheGetAttr(AUTHNAME, roleTup,
ReleaseSysCache(roleTup);
*logdetail = psprintf(_("User \"%s\" has no password assigned."),
role);
- return STATUS_ERROR; /* user has no password */
+ return NULL; /* user has no password */
}
- *shadow_pass = TextDatumGetCString(datum);
+ shadow_pass = TextDatumGetCString(datum);
datum = SysCacheGetAttr(AUTHNAME, roleTup,
Anum_pg_authid_rolvaliduntil, &isnull);
ReleaseSysCache(roleTup);
- if (**shadow_pass == '\0')
+ if (*shadow_pass == '\0')
{
*logdetail = psprintf(_("User \"%s\" has an empty password."),
role);
- pfree(*shadow_pass);
- *shadow_pass = NULL;
- return STATUS_ERROR; /* empty password */
+ pfree(shadow_pass);
+ return NULL; /* empty password */
}
/*
- * Password OK, now check to be sure we are not past rolvaliduntil
+ * Password OK, but check to be sure we are not past rolvaliduntil
*/
- if (isnull)
- retval = STATUS_OK;
- else if (vuntil < GetCurrentTimestamp())
+ if (!isnull && vuntil < GetCurrentTimestamp())
{
*logdetail = psprintf(_("User \"%s\" has an expired password."),
role);
- retval = STATUS_ERROR;
+ return NULL;
}
- else
- retval = STATUS_OK;
- return retval;
+ return shadow_pass;
}
/*
parsedline->auth_method = uaMD5;
}
else if (strcmp(token->string, "scram") == 0)
- parsedline->auth_method = uaSASL;
+ parsedline->auth_method = uaSCRAM;
else if (strcmp(token->string, "pam") == 0)
#ifdef USE_PAM
parsedline->auth_method = uaPAM;
extern char *encrypt_password(PasswordType target_type, const char *role,
const char *password);
-extern int get_role_password(const char *role, char **shadow_pass,
- char **logdetail);
+extern char *get_role_password(const char *role, char **logdetail);
extern int md5_crypt_verify(const char *role, const char *shadow_pass,
const char *client_pass, const char *md5_salt,
uaIdent,
uaPassword,
uaMD5,
- uaSASL,
+ uaSCRAM,
uaGSS,
uaSSPI,
uaPAM,
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-extern void *pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed);
+extern void *pg_be_scram_init(const char *username, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
test_role($node, 'md5_role', 'scram', 2);
test_role($node, 'plain_role', 'scram', 0);
- # For "md5" method, users "plain_role" and "md5_role" should be able to
- # connect.
+ # For "md5" method, all users should be able to connect (SCRAM
+ # authentication will be performed for the user with a scram verifier.)
reset_pg_hba($node, 'md5');
- test_role($node, 'scram_role', 'md5', 2);
+ test_role($node, 'scram_role', 'md5', 0);
test_role($node, 'md5_role', 'md5', 0);
test_role($node, 'plain_role', 'md5', 0);
}