org.postgresql.sspi.SSPIClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of postgresql Show documentation
Show all versions of postgresql Show documentation
PostgreSQL JDBC Driver Postgresql
/*
* Copyright (c) 2003, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
// Copyright (c) 2004, Open Cloud Limited.
package org.postgresql.sspi;
import static org.postgresql.util.internal.Nullness.castNonNull;
import org.postgresql.core.PGStream;
import org.postgresql.util.HostSpec;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import com.sun.jna.LastErrorException;
import com.sun.jna.Platform;
import com.sun.jna.platform.win32.Sspi;
import com.sun.jna.platform.win32.Sspi.SecBufferDesc;
import com.sun.jna.platform.win32.Win32Exception;
import org.checkerframework.checker.nullness.qual.Nullable;
import waffle.windows.auth.IWindowsCredentialsHandle;
import waffle.windows.auth.impl.WindowsCredentialsHandleImpl;
import waffle.windows.auth.impl.WindowsSecurityContextImpl;
import java.io.IOException;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Use Waffle-JNI to support SSPI authentication when PgJDBC is running on a Windows client and
* talking to a Windows server.
*
* SSPI is not supported on a non-Windows client.
*
* @author craig
*/
public class SSPIClient implements ISSPIClient {
public static final String SSPI_DEFAULT_SPN_SERVICE_CLASS = "POSTGRES";
private static final Logger LOGGER = Logger.getLogger(SSPIClient.class.getName());
private final PGStream pgStream;
private final String spnServiceClass;
private final boolean enableNegotiate;
private @Nullable IWindowsCredentialsHandle clientCredentials;
private @Nullable WindowsSecurityContextImpl sspiContext;
private @Nullable String targetName;
/**
* Instantiate an SSPIClient for authentication of a connection.
*
* SSPIClient is not re-usable across connections.
*
* It is safe to instantiate SSPIClient even if Waffle and JNA are missing or on non-Windows
* platforms, however you may not call any methods other than isSSPISupported().
*
* @param pgStream PostgreSQL connection stream
* @param spnServiceClass SSPI SPN service class, defaults to POSTGRES if null
* @param enableNegotiate enable negotiate
*/
public SSPIClient(PGStream pgStream, String spnServiceClass, boolean enableNegotiate) {
this.pgStream = pgStream;
if (spnServiceClass == null || spnServiceClass.isEmpty()) {
spnServiceClass = SSPI_DEFAULT_SPN_SERVICE_CLASS;
}
this.spnServiceClass = spnServiceClass;
/* If we're forcing Kerberos (no spnego), disable SSPI negotiation */
this.enableNegotiate = enableNegotiate;
}
/**
* Test whether we can attempt SSPI authentication. If false, do not attempt to call any other
* SSPIClient methods.
*
* @return true if it's safe to attempt SSPI authentication
*/
@Override
public boolean isSSPISupported() {
try {
/*
* SSPI is windows-only. Attempt to use JNA to identify the platform. If Waffle is missing we
* won't have JNA and this will throw a NoClassDefFoundError.
*/
if (!Platform.isWindows()) {
LOGGER.log(Level.FINE, "SSPI not supported: non-Windows host");
return false;
}
/* Waffle must be on the CLASSPATH */
Class.forName("waffle.windows.auth.impl.WindowsSecurityContextImpl");
return true;
} catch (NoClassDefFoundError ex) {
LOGGER.log(Level.WARNING, "SSPI unavailable (no Waffle/JNA libraries?)", ex);
return false;
} catch (ClassNotFoundException ex) {
LOGGER.log(Level.WARNING, "SSPI unavailable (no Waffle/JNA libraries?)", ex);
return false;
}
}
private String makeSPN() throws PSQLException {
final HostSpec hs = pgStream.getHostSpec();
try {
/*
The GSSAPI implementation does not use the port in the service name.
Force the port number to 0
Fixes issue 1482
*/
return NTDSAPIWrapper.instance.DsMakeSpn(spnServiceClass, hs.getHost(), null,
(short) 0, null);
} catch (LastErrorException ex) {
throw new PSQLException("SSPI setup failed to determine SPN",
PSQLState.CONNECTION_UNABLE_TO_CONNECT, ex);
}
}
/**
* Respond to an authentication request from the back-end for SSPI authentication (AUTH_REQ_SSPI).
*
* @throws SQLException on SSPI authentication handshake failure
* @throws IOException on network I/O issues
*/
@Override
public void startSSPI() throws SQLException, IOException {
/*
* We usually use SSPI negotiation (spnego), but it's disabled if the client asked for GSSPI and
* usespngo isn't explicitly turned on.
*/
final String securityPackage = enableNegotiate ? "negotiate" : "kerberos";
LOGGER.log(Level.FINEST, "Beginning SSPI/Kerberos negotiation with SSPI package: {0}", securityPackage);
try {
/*
* Acquire a handle for the local Windows login credentials for the current user
*
* See AcquireCredentialsHandle
* (http://msdn.microsoft.com/en-us/library/windows/desktop/aa374712%28v=vs.85%29.aspx)
*
* This corresponds to pg_SSPI_startup in libpq/fe-auth.c .
*/
IWindowsCredentialsHandle clientCredentials;
try {
clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
this.clientCredentials = clientCredentials;
clientCredentials.initialize();
} catch (Win32Exception ex) {
throw new PSQLException("Could not obtain local Windows credentials for SSPI",
PSQLState.CONNECTION_UNABLE_TO_CONNECT /* TODO: Should be authentication error */, ex);
}
try {
String targetName = makeSPN();
this.targetName = targetName;
LOGGER.log(Level.FINEST, "SSPI target name: {0}", targetName);
sspiContext = new WindowsSecurityContextImpl();
sspiContext.setPrincipalName(targetName);
sspiContext.setCredentialsHandle(clientCredentials);
sspiContext.setSecurityPackage(securityPackage);
sspiContext.initialize(null, null, targetName);
} catch (Win32Exception ex) {
throw new PSQLException("Could not initialize SSPI security context",
PSQLState.CONNECTION_UNABLE_TO_CONNECT /* TODO: Should be auth error */, ex);
}
sendSSPIResponse(sspiContext.getToken());
LOGGER.log(Level.FINEST, "Sent first SSPI negotiation message");
} catch (NoClassDefFoundError ex) {
throw new PSQLException(
"SSPI cannot be used, Waffle or its dependencies are missing from the classpath",
PSQLState.NOT_IMPLEMENTED, ex);
}
}
/**
* Continue an existing authentication conversation with the back-end in response to an
* authentication request of type AUTH_REQ_GSS_CONT.
*
* @param msgLength Length of message to read, excluding length word and message type word
* @throws SQLException if something wrong happens
* @throws IOException if something wrong happens
*/
@Override
public void continueSSPI(int msgLength) throws SQLException, IOException {
WindowsSecurityContextImpl sspiContext = this.sspiContext;
if (sspiContext == null) {
throw new IllegalStateException("Cannot continue SSPI authentication that we didn't begin");
}
LOGGER.log(Level.FINEST, "Continuing SSPI negotiation");
/* Read the response token from the server */
byte[] receivedToken = pgStream.receive(msgLength);
SecBufferDesc continueToken = new SecBufferDesc(Sspi.SECBUFFER_TOKEN, receivedToken);
sspiContext.initialize(sspiContext.getHandle(), continueToken, castNonNull(targetName));
/*
* Now send the response token. If negotiation is complete there may be zero bytes to send, in
* which case we shouldn't send a reply as the server is not expecting one; see fe-auth.c in
* libpq for details.
*/
byte[] responseToken = sspiContext.getToken();
if (responseToken.length > 0) {
sendSSPIResponse(responseToken);
LOGGER.log(Level.FINEST, "Sent SSPI negotiation continuation message");
} else {
LOGGER.log(Level.FINEST, "SSPI authentication complete, no reply required");
}
}
private void sendSSPIResponse(byte[] outToken) throws IOException {
/*
* The sspiContext now contains a token we can send to the server to start the handshake. Send a
* 'password' message containing the required data; the server knows we're doing SSPI
* negotiation and will deal with it appropriately.
*/
pgStream.sendChar('p');
pgStream.sendInteger4(4 + outToken.length);
pgStream.send(outToken);
pgStream.flush();
}
/**
* Clean up native win32 resources after completion or failure of SSPI authentication. This
* SSPIClient instance becomes unusable after disposal.
*/
@Override
public void dispose() {
if (sspiContext != null) {
sspiContext.dispose();
sspiContext = null;
}
if (clientCredentials != null) {
clientCredentials.dispose();
clientCredentials = null;
}
}
}