Skip to content
Permalink
Browse files

Add password callback to NpgsqlConnection (#2502)

New connections will execute the delegate to obtain a password.
This allows the use of dynamic passwords or tokens such as those used by Amazon RDS IAM Authentication.

Fixes #2500
  • Loading branch information...
williamdenton authored and roji committed Jun 29, 2019
1 parent ca75272 commit d78f3d6c8ca88d6adb6fb26d77bd8d8293269d0c
@@ -6,6 +6,8 @@ The simplest way to log into PostgreSQL is by specifying a `Username` and a `Pas

If a `Password` is not specified and your PostgreSQL is configured to request a password (plain or MD5), Npgsql will look for a [standard PostgreSQL password file](https://www.postgresql.org/docs/current/static/libpq-pgpass.html). If you specify the `Passfile` connection string parameter, the file it specifies will be used. If that parameter isn't defined, Npgsql will look under the path taken from `PGPASSFILE` environment variable. If the environment variable isn't defined, Npgsql will fall back to the system-dependent default directory which is `$HOME/.pgpass` for Unix and `%APPDATA%\postgresql\pgpass.conf` for Windows.

`NpgsqlConnection` can also be configured with a `ProvidePasswordCallback`. This will be executed when new database connections are opened to generate a password in code. This can be useful if you are using Amazon Web Services RDS for Postgres which can be configured to use short lived tokens generated based on access credentials. The `ProvidePasswordCallback` delegate is executed when both password and passFile connection string parameters are not specified.

For documentation about all auth methods supported by PostgreSQL, [see this page](http://www.postgresql.org/docs/current/static/auth-methods.html). Note that Npgsql supports Unix-domain sockets (auth method `local`), simply set your `Host` parameter to the absolute path of your PostgreSQL socket directory, as configred in your `postgresql.conf`.

## Integrated Security (GSS/SSPI/Kerberos)
@@ -330,6 +330,20 @@ public override string ConnectionString
}
}

/// <summary>
/// Gets or sets the delegate used to generate a password for new database connections.
/// </summary>
/// <remarks>
/// This delegate is executed when a new database connection is opened that requires a password.
/// <see cref="NpgsqlConnectionStringBuilder.Password">Password</see> and
/// <see cref="NpgsqlConnectionStringBuilder.Passfile">Passfile</see> connection string
/// properties have precedence over this delegate. It will not be executed if a password is
/// specified, or the specified or default Passfile contains a valid entry.
/// Due to connection pooling this delegate is only executed when a new physical connection
/// is opened, not when reusing a connection that was previously opened from the pool.
/// </remarks>
public ProvidePasswordCallback? ProvidePasswordCallback { get; set; }

#endregion Connection string management

#region Configuration settings
@@ -1415,5 +1429,15 @@ public void ReloadTypes()
/// <param name="certificates">A <see cref="System.Security.Cryptography.X509Certificates.X509CertificateCollection">X509CertificateCollection</see> to be filled with one or more client certificates.</param>
public delegate void ProvideClientCertificatesCallback(X509CertificateCollection certificates);

/// <summary>
/// Represents the method that allows the application to provide a password at connection time in code rather than configuration
/// </summary>
/// <param name="host">Hostname</param>
/// <param name="port">Port</param>
/// <param name="database">Database Name</param>
/// <param name="username">User</param>
/// <returns>A valid password for connecting to the database</returns>
public delegate string ProvidePasswordCallback(string host, int port, string database, string username);

#endregion
}
@@ -27,15 +27,15 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async)
return;

case AuthenticationRequestType.AuthenticationCleartextPassword:
await AuthenticateCleartext(async);
await AuthenticateCleartext(username, async);
return;

case AuthenticationRequestType.AuthenticationMD5Password:
await AuthenticateMD5(username, ((AuthenticationMD5PasswordMessage)msg).Salt, async);
return;

case AuthenticationRequestType.AuthenticationSASL:
await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, async);
await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, username, async);
return;

case AuthenticationRequestType.AuthenticationGSS:
@@ -51,9 +51,9 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async)
}
}

async Task AuthenticateCleartext(bool async)
async Task AuthenticateCleartext(string username, bool async)
{
var passwd = GetPassword();
var passwd = GetPassword(username);
if (passwd == null)
throw new NpgsqlException("No password has been provided but the backend requires one (in cleartext)");

@@ -65,15 +65,15 @@ async Task AuthenticateCleartext(bool async)
Expect<AuthenticationRequestMessage>(await ReadMessage(async));
}

async Task AuthenticateSASL(List<string> mechanisms, bool async)
async Task AuthenticateSASL(List<string> mechanisms, string username, bool async)
{
// At the time of writing PostgreSQL only supports SCRAM-SHA-256
if (!mechanisms.Contains("SCRAM-SHA-256"))
throw new NpgsqlException("No supported SASL mechanism found (only SCRAM-SHA-256 is supported for now). " +
"Mechanisms received from server: " + string.Join(", ", mechanisms));
var mechanism = "SCRAM-SHA-256";

var passwd = GetPassword() ??
var passwd = GetPassword(username) ??
throw new NpgsqlException($"No password has been provided but the backend requires one (in SASL/{mechanism})");

const string ClientKey = "Client Key";
@@ -179,7 +179,7 @@ static byte[] HMAC(byte[] data, string key)

async Task AuthenticateMD5(string username, byte[] salt, bool async)
{
var passwd = GetPassword();
var passwd = GetPassword(username);
if (passwd == null)
throw new NpgsqlException("No password has been provided but the backend requires one (in MD5)");

@@ -366,20 +366,33 @@ public override long Position

class AuthenticationCompleteException : Exception { }

string? GetPassword()
string? GetPassword(string username)
{
var passwd = Settings.Password;
if (passwd != null)
return passwd;

// No password was provided. Attempt to pull the password from the pgpass file.
var matchingEntry = PgPassFile.Load(Settings.Passfile)?.GetFirstMatchingEntry(Settings.Host, Settings.Port, Settings.Database, Settings.Username);
var matchingEntry = PgPassFile.Load(Settings.Passfile)?.GetFirstMatchingEntry(Host, Port, Settings.Database!, username);
if (matchingEntry != null)
{
Log.Trace("Taking password from pgpass file");
return matchingEntry.Password;
}

if (ProvidePasswordCallback != null)
{
Log.Trace($"Taking password from {nameof(ProvidePasswordCallback)} delegate");
try
{
return ProvidePasswordCallback(Host, Port, Settings.Database!, username);
}
catch (Exception e)
{
throw new NpgsqlException($"Obtaining password using {nameof(NpgsqlConnection)}.{nameof(ProvidePasswordCallback)} delegate failed", e);
}
}

return null;
}
}
@@ -50,6 +50,7 @@ sealed partial class NpgsqlConnector : IDisposable

ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; }
RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
ProvidePasswordCallback? ProvidePasswordCallback { get; }

internal Encoding TextEncoding { get; private set; } = default!;

@@ -239,13 +240,15 @@ internal NpgsqlConnector(NpgsqlConnection connection)
Connection.Connector = this;
ProvideClientCertificatesCallback = Connection.ProvideClientCertificatesCallback;
UserCertificateValidationCallback = Connection.UserCertificateValidationCallback;
ProvidePasswordCallback = Connection.ProvidePasswordCallback;
}

NpgsqlConnector(NpgsqlConnector connector)
: this(connector.Settings, connector.ConnectionString)
{
ProvideClientCertificatesCallback = connector.ProvideClientCertificatesCallback;
UserCertificateValidationCallback = connector.UserCertificateValidationCallback;
ProvidePasswordCallback = connector.ProvidePasswordCallback;
}

/// <summary>
@@ -183,6 +183,108 @@ public void AuthenticationFailure()
}
}

#region ProvidePasswordCallback Tests

[Test, Description("ProvidePasswordCallback is used when password is not supplied in connection string")]
public void ProvidePasswordCallbackDelegateIsUsed()
{
var connString = new NpgsqlConnectionStringBuilder(ConnectionString)
{
Pooling = false //testing opening of connections, pooling will return an existing connection
};
var goodPassword = connString.Password;
connString.Password = null;

bool getPasswordDelegateWasCalled = false;

using (var conn = new NpgsqlConnection(connString.ToString()) { ProvidePasswordCallback = ProvidePasswordCallback })
{
conn.Open();
Assert.True(getPasswordDelegateWasCalled, "ProvidePasswordCallback delegate not used");
}

string ProvidePasswordCallback(string host, int port, string database, string username)
{
getPasswordDelegateWasCalled = true;
return goodPassword;
}
}

[Test, Description("ProvidePasswordCallback is not used when password is supplied in connection string")]
public void ProvidePasswordCallbackDelegateIsNotUsed()
{
var connString = new NpgsqlConnectionStringBuilder(ConnectionString)
{
Pooling = false //testing opening of connections, pooling will return an existing connection
};

using (var conn = new NpgsqlConnection(connString.ToString()) { ProvidePasswordCallback = ProvidePasswordCallback })
{
Assert.DoesNotThrow(() => conn.Open());
}

string ProvidePasswordCallback(string host, int port, string database, string username)
{
throw new Exception("password should come from connection string, not delegate");
}
}

[Test, Description("Exceptions thrown from client application are wrapped when using ProvidePasswordCallback Delegate")]
public void ProvidePasswordCallbackDelegateExceptionsAreWrapped()
{
var connString = new NpgsqlConnectionStringBuilder(ConnectionString)
{
Pooling = false, //testing opening of connections, pooling will return an existing connection
Password = null
};

using (var conn = new NpgsqlConnection(connString.ToString()) { ProvidePasswordCallback = ProvidePasswordCallback })
{
Assert.That(() => conn.Open(), Throws.Exception
.TypeOf<NpgsqlException>()
.With.InnerException.Message.EqualTo("inner exception from ProvidePasswordCallback")
);
}

string ProvidePasswordCallback(string host, int port, string database, string username)
{
throw new Exception("inner exception from ProvidePasswordCallback");
}
}

[Test, Description("Parameters passed to ProvidePasswordCallback delegate are correct")]
public void ProvidePasswordCallbackDelegateGetsCorrectArguments()
{
var connString = new NpgsqlConnectionStringBuilder(ConnectionString) { Pooling = false };
var goodPassword = connString.Password;
connString.Password = null;

string? receivedHost = null;
int? receivedPort = null;
string? receivedDatabase = null;
string? receivedUsername = null;

using (var conn = new NpgsqlConnection(connString.ToString()) { ProvidePasswordCallback = ProvidePasswordCallback })
{
conn.Open();
Assert.AreEqual(connString.Host, receivedHost);
Assert.AreEqual(connString.Port, receivedPort);
Assert.AreEqual(connString.Database, receivedDatabase);
Assert.AreEqual(connString.Username, receivedUsername);
}

string ProvidePasswordCallback(string host, int port, string database, string username)
{
receivedHost = host;
receivedPort = port;
receivedDatabase = database;
receivedUsername = username;

return goodPassword;
}
}
#endregion

[Test]
public void BadDatabase()
{

0 comments on commit d78f3d6

Please sign in to comment.
You can’t perform that action at this time.