Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a plugin manager interface to allow different mechanisms to acquire a password. #2321

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -139,6 +139,7 @@ In addition to the standard connection parameters the driver supports a number o
| adaptiveFetchMaximum | Integer | -1 | Specifies maximum number of rows, which can be calculated by adaptiveFetch. Number of rows used by adaptiveFetch cannot go above this value. Any negative number set as adaptiveFetchMaximum is used by adaptiveFetch as infinity number of rows.
| localSocketAddress | String | null | Hostname or IP address given to explicitly configure the interface that the driver will bind the client side of the TCP/IP connection to when connecting.
| quoteReturningIdentifiers | Boolean | true | By default we double quote returning identifiers. Some ORM's already quote them. Switch allows them to turn this off
| authenticationPluginClassName | STRING | null | fully qualified class name of the class implementing the AuthenticationPlugin interface. If this is null, the default PasswordAuthentication plugin will be used

## Contributing
For information on how to contribute to the project see the [Contributing Guidelines](CONTRIBUTING.md)
Expand Down
197 changes: 101 additions & 96 deletions docs/documentation/head/connect.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/PGProperty.java
Expand Up @@ -82,6 +82,16 @@ public enum PGProperty {
null,
"Assume the server is at least that version"),

/**
* AuthenticationPluginClass
*/

AUTHENTICATION_PLUGIN_CLASS_NAME(
"authenticationPluginClassName",
null,
"Name of class which implements AuthenticationPlugin"
),

/**
* Specifies what the driver should do if a query fails. In {@code autosave=always} mode, JDBC driver sets a savepoint before each query,
* and rolls back to that savepoint in case of failure. In {@code autosave=never} mode (default), no savepoint dance is made ever.
Expand Down
10 changes: 10 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/AuthenticationPlugin.java
@@ -0,0 +1,10 @@
/*
* Copyright (c) 2021, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.core;

public interface AuthenticationPlugin {
byte[] getEncodedPassword(String userName, String password);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably need some javadoc here.
for example, this asks for encoded password, but what encoding?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough. This is really a work in progress.

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer somewhat more generic interface that would include different auth-methods that PG supports today and some future looking like oauth2 access token that are currently commonly implemented through overloaded password field (though they are nothing like a passwords), and in future would hopefully get it's own auth-method (but keep the existing plugin ;) )

public interface AuthenticationPlugin {
  AuthenticationInfo getAuthenticationInfo(Properties info) throws PSQLException, PSQLException;
}

public class AuthenticationInfo {
  final String authenticationType;
  final byte[] credential;

   AuthenticationInfo (String authenticationType, byte[] credential) {
        this.authenticationType= authenticationType;
        this.credential= credential;
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you need to know the auth method? I would think at the end of the day all we need is the password?
How would I use the AuthenticationInfo ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you scope AuthenticationPlugin only to Password auth-method then you would not need that info (and you may call plugin PasswordAuthenticationPlugin).

My proposal is to make plugin applicable to the other auth-methods as well. Depending on authenticationType the driver would put credential (password) in use differently.

That said, below may be a better interface.

public interface AuthenticationPlugin {
    String getAuthenticationType(Properties info);
    byte[] getCredential(Properties info) throws PSQLException, PSQLException;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would getAuthenticationType(...) return? Is that supposed to be the server auth method? If so, that would not work as the server decides how it wants the client to authenticate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intended use is that if you want a different authentication mechanism you load it via Class.forName() and acquire your password any way you wish. Ultimately the server only needs the password bytes. As @sehrope mentioned there is no real way to determine many of the auth methods ahead of time.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case server determines the method should we have something like this?

public interface AuthenticationPlugin {
    boolean isSupported(String authenticationType, Properties info);
    byte[] getCredential(String authenticationType, Properties info) throws PSQLException, PSQLException;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently one place where the server determines that we would use GSS encryption, but not authentication. I don't see anywhere else in the protocol where this happens.

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2021, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.core;

import org.postgresql.PGProperty;

import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AuthenticationPluginManager {

private static final Logger LOGGER = Logger.getLogger(AuthenticationPluginManager.class.getName());

public static AuthenticationPlugin getAuthenticationPlugin(Properties info) throws Exception {
String authenticationClassName = PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.getSetString(info);

if ( authenticationClassName == null ) {
return new PasswordAuthentication();
} else {
davecramer marked this conversation as resolved.
Show resolved Hide resolved
try {
return (AuthenticationPlugin) Class.forName(authenticationClassName).getDeclaredConstructor().newInstance();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does there need to be consideration about what classloader to use?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should match however we handle other class parameters like socket factories.

We should also either add an "authManagerPluginArg" parameter to allow the caller to customize the instantiated class, e.g. new FooAuthPlugin(String arg).

Otherwise we'd be relying on them passing extra properties into the Properties object that we would ignore in the Driver itself. But if there's no standard for naming those, there's no guarantee we don't eventually add them as "real" properties. Maybe a prefixed name like "ext." to signify end user properties that we'll allow to exist in the Properties map but otherwise ignore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding args to the plugin is interesting. We would have to pass the arg into getAuthenticationPlugin
So one option is to provide a second getAuthenticationPlugin(Properties props, Object arg) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the Properties alone is fine as the user can retrieve the value if they want it. They just need a factory name, like sslfactoryarg but specifically for this.

We could add a separate constructor that takes a String (again similar to the SSL factories) but it's not strictly necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just follow the same pattern as the SSL factories (and use the ObjectFactory logic)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's a good idea. All this class instantiation should happen in one place so the ClassLoader et al are handled consistently.

} catch (Exception ex ) {
LOGGER.log(Level.FINE, "Unable to load Authentication Plugin" + ex.getMessage() );
davecramer marked this conversation as resolved.
Show resolved Hide resolved
throw ex;
}
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2021, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.core;

import java.nio.charset.StandardCharsets;

public class PasswordAuthentication implements AuthenticationPlugin {

@Override
public byte[] getEncodedPassword(String userName, String password) {
return password.getBytes(StandardCharsets.UTF_8);
}
}
Expand Up @@ -9,6 +9,8 @@
import static org.postgresql.util.internal.Nullness.castNonNull;

import org.postgresql.PGProperty;
import org.postgresql.core.AuthenticationPlugin;
import org.postgresql.core.AuthenticationPluginManager;
import org.postgresql.core.ConnectionFactory;
import org.postgresql.core.PGStream;
import org.postgresql.core.QueryExecutor;
Expand Down Expand Up @@ -95,6 +97,7 @@ private PGStream tryConnect(String user, String database,
Properties info, SocketFactory socketFactory, HostSpec hostSpec,
SslMode sslMode, GSSEncMode gssEncMode)
throws SQLException, IOException {

int connectTimeout = PGProperty.CONNECT_TIMEOUT.getInt(info) * 1000;

PGStream newStream = new PGStream(socketFactory, hostSpec, connectTimeout);
Expand Down Expand Up @@ -595,6 +598,13 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
// Now get the response from the backend, either an error message
// or an authentication request

AuthenticationPlugin authenticationPlugin;
try {
authenticationPlugin = AuthenticationPluginManager.getAuthenticationPlugin(info);
} catch ( Exception ex ) {
throw new PSQLException(ex.getMessage(), PSQLState.UNEXPECTED_ERROR);
}

String password = PGProperty.PASSWORD.get(info);

/* SSPI negotiation state, if used */
Expand Down Expand Up @@ -672,7 +682,7 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
PSQLState.CONNECTION_REJECTED);
}

byte[] encodedPassword = password.getBytes(StandardCharsets.UTF_8);
byte[] encodedPassword = authenticationPlugin.getEncodedPassword(user, password);

pgStream.sendChar('p');
pgStream.sendInteger4(4 + encodedPassword.length + 1);
Expand Down
20 changes: 20 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
Expand Up @@ -1305,6 +1305,26 @@ public void setURL(String url) {
setUrl(url);
}

/**
*
* @return the class name to use for the Authentication Plugin.
* This can be null in which case the default password authentication plugin will be used
*/
public @Nullable String getAuthenticationPluginClassName() {
return PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.get(properties);
}

/**
*
* @param className name of a class which implements {@link org.postgresql.core.AuthenticationPlugin}
* This class will be used to get the encoded bytes to be sent to the server as the
* password to authenticate the user.
*
*/
public void setAuthenticationPluginClassName(String className) {
PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.set(properties, className);
}

public @Nullable String getProperty(String name) throws SQLException {
PGProperty pgProperty = PGProperty.forName(name);
if (pgProperty != null) {
Expand Down