Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 70 additions & 5 deletions src/main/java/com/ibm/as400/access/AS400.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.net.UnknownHostException;
import java.net.URL;
import java.util.Arrays;
import java.util.Base64;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.Locale;
Expand Down Expand Up @@ -388,6 +389,48 @@ public class AS400 implements Serializable, AutoCloseable
private boolean forcePrompt_ = false;
private int validateSignonTimeOut_ = 0;

private transient CredentialVault kerbTicket_;

// Prefix used to indicate that the password contains a base64-encoded Kerberos token.
public static final String KERBEROS_PREFIX = "_KERBEROSAUTH_";
Copy link
Member

Choose a reason for hiding this comment

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

The whole logic around KERBEROS_PREFIX is actually a pretty cool feature, but it still seems non-optimal to conflate kerberos tickets with the password.

As implemented, it allows non-programmatic connections like JDBC to use a kerberos ticket, very useful.

More importantly, the Mapepire client uses this new _KERBEROSAUTH_ scheme. See https://github.com/Mapepire-IBMi/mapepire-python/pull/84/files

So if we redesign this, we need to also redesign the Mapepire client, or at least the Mapepire server (middleware). In the Mapepire case, the ultimate constraining factor is that we're passing this through an HTTP basic auth mechanism which only supports userid and password. Hence the reason the prefix was invented. It's hacky and potentially problematic in the unlikely case someone has a password starting with this prefix, but it's honestly the best we can do.

My intuition says we should do the following:

@julesyan + @nadiramra , thoughts?

Copy link
Member

@ThePrez ThePrez Dec 29, 2025

Choose a reason for hiding this comment

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

I started looking at pulling the necessary changes into the mapepire-server project, but it's more complex than initially thought. Mapepire-server gets its connection through DriverManager.getConnection() which has no support for sending a kerberos token.

I see why it was implemented in this way, to support Kerberos through the standard DriverManager classes. Which I still assert is a useful feature.

So I think this boils down to three high-level options:

Option 1: existing implementation
Pros:

  • We know it works, and it covers all our bases

Cons:

  • conflates kerberos ticket and password

Option 2: change mapepire-server to not use DriverManager interfaces
Pros:

  • Pretty easy to implement
  • keeps password and kerberos ticket separate

Cons:

  • Creates a dual maintenance path for future versions of the server if we exoect to leverage other JDBC drivers
  • Makes use of native JDBC driver more complicated
  • If we've made it this far, we might as well publish a new feature of JDBC support for kerberos!

Option 3: create a new JDBC property for kerberos ticket (I believe this is best)
Pros:

  • keeps password and kerberos ticket separate
  • unlikely to have backward/forward compatibility issues

Cons:

  • Adding a new connection property is more involved

@julesyan / @nadiramra / @jeber-ibm , thoughts?

public static final char[] KERBEROS_PREFIX_CHARS = KERBEROS_PREFIX.toCharArray();

private void setKerbTicket(byte[] ticket) {
this.kerbTicket_ = new PasswordVault(ticket);
}

public void clearKerbTicket() {
if (!this.kerbTicket_.isEmpty())
this.kerbTicket_.empty();
}

// Determines if the password contains a Kerberos token
private boolean isKerbTicket(char[] auth){
char[] prefix = KERBEROS_PREFIX_CHARS;
if (auth == null || auth.length < prefix.length) {
return false;
}

for (int i = 0; i < prefix.length; i++) {
if (auth[i] != prefix[i]) {
return false;
}
}
return true;
}

// Extracts the Kerberos token from the password
private static char[] getKerbTicketFromPassword(char[] password) {
int prefixLen = KERBEROS_PREFIX_CHARS.length;
int tokenLen = password.length - prefixLen;

char[] tokenChars = new char[tokenLen];
System.arraycopy(password, prefixLen, tokenChars, 0, tokenLen);

return tokenChars;
}


/**
* Constructs an AS400 object.
* <p>
Expand Down Expand Up @@ -647,7 +690,6 @@ public AS400(String systemName, String userId, char[] password)
if (userId.length() > 10)
throw new ExtendedIllegalArgumentException("userId (" + userId + ")", ExtendedIllegalArgumentException.LENGTH_NOT_VALID);

checkPasswordNullAndLength(password, "password");
construct();
systemName_ = systemName;
systemNameLocal_ = resolveSystemNameLocal(systemName);
Expand All @@ -658,7 +700,17 @@ public AS400(String systemName, String userId, char[] password)
}

userId_ = userId.toUpperCase();
credVault_ = new PasswordVault(password);
// Create appropriate credential vault based on whether the password is a Kerberos token or a regular password.
boolean isKerberosTicket = isKerbTicket(password);
if (isKerberosTicket){
password = getKerbTicketFromPassword(password);
credVault_ = new GSSTokenVault();

this.setKerbTicket(Base64.getDecoder().decode((new String(password))));
}else{
checkPasswordNullAndLength(password, "password");
credVault_ = new PasswordVault(password);
}
proxyServer_ = resolveProxyServer(proxyServer_);
}

Expand Down Expand Up @@ -1794,6 +1846,10 @@ private synchronized void chooseImpl()
}
}

// If kerbTicket_ has been set, make sure the impl knows about it.
if (!kerbTicket_.isEmpty())
impl_.setKerbTicket(kerbTicket_.getClearCredential());

if (!propertiesFrozen_)
{
impl_.setState(useSSLConnection_, canUseNativeOptimizations(), threadUsed_, ccsid_, nlv_,
Expand Down Expand Up @@ -4145,6 +4201,7 @@ public void removeVetoableChangeListener(VetoableChangeListener listener)
public synchronized void resetAllServices()
{
if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Resetting all services.");
clearKerbTicket();
setStayAlive(0);

disconnectAllServices();
Expand Down Expand Up @@ -5443,9 +5500,17 @@ synchronized void signon(boolean keepConnection) throws AS400SecurityException,
&& (credVault_.getType() == AUTHENTICATION_SCHEME_GSS_TOKEN || gssOption_ != AS400.GSS_OPTION_NONE))
{
// Try for Kerberos.
byte[] newBytes = (gssCredential_ == null) ? TokenManager.getGSSToken(systemName_, gssName_) :
TokenManager2.getGSSToken(systemName_, gssCredential_);

byte[] newBytes = null;

if (!kerbTicket_.isEmpty() && kerbTicket_.getClearCredential().length > 0) {
if (Trace.traceOn_) Trace.log(Trace.DIAGNOSTIC, "Using injected Kerberos ticket.");
newBytes = kerbTicket_.getClearCredential();
} else {
// Fall back to generating the token normally
newBytes = (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName_)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
}
// We do not have to empty the existing vault because the
// previous if-check assures us it is already empty.
credVault_ = new GSSTokenVault(newBytes);
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/ibm/as400/access/AS400Impl.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,7 @@ interface AS400Impl
String getSystemName();
/* Set the VRM for the object. Only set for the remote Impl */
void setVRM(int v, int r, int m);


void setKerbTicket(byte[] ticket);
}
7 changes: 7 additions & 0 deletions src/main/java/com/ibm/as400/access/AS400ImplProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ public SignonInfo skipSignon(String systemName, boolean systemNameLocal, String


private int bidiStringType = BidiStringType.DEFAULT;
private CredentialVault kerbTicket_;

/**
* Sets bidi string type of the connection.
Expand Down Expand Up @@ -404,4 +405,10 @@ public void setVRM(int v, int r, int m) {
public void setAdditionalAuthenticationFactor(char[] additionalAuthFactor) {
// Does nothing for the proxy class
}

@Override
public void setKerbTicket(byte[] ticket) {
this.kerbTicket_ = new PasswordVault(ticket);
}

}
31 changes: 26 additions & 5 deletions src/main/java/com/ibm/as400/access/AS400ImplRemote.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ public class AS400ImplRemote implements AS400Impl

private static final String CLASSNAME = "com.ibm.as400.access.AS400ImplRemote";

// GSS Token, for Kerberos.
private CredentialVault kerbTicket_;

static {
if (Trace.traceOn_)
Trace.logLoadPath(CLASSNAME);
Expand Down Expand Up @@ -665,10 +668,14 @@ public int createUserHandle() throws AS400SecurityException, IOException
{
try
{
byte[] authenticationBytes = (gssCredential_ == null)
byte[] authenticationBytes;
if (!this.kerbTicket_.isEmpty()){
authenticationBytes = this.kerbTicket_.getClearCredential();
} else {
authenticationBytes = (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName_)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
}
IFSUserHandle2Req req = new IFSUserHandle2Req(authenticationBytes, aafIndicator_ ? additionalAuthFactor_ : null);
ds = (ClientAccessDataStream) connectedServer.sendAndReceive(req);
}
Expand Down Expand Up @@ -1016,9 +1023,13 @@ public void generateProfileToken(ProfileTokenCredential profileToken, String use
case AS400.AUTHENTICATION_SCHEME_GSS_TOKEN:
Copy link
Member

Choose a reason for hiding this comment

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

What we really should do is define a new constant and associated switch case here, AS400.AUTHENTICATION_SCHEME_KRB_TICKET

This is more pure as it keeps the direct-kerb-ticket path and the GSS path separate

try
{
authenticationBytes = (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
if (!this.kerbTicket_.isEmpty()){
Copy link
Member

Choose a reason for hiding this comment

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

this change (and other similar changes) would slide into the new switch statement(s) for AUTHENTICATION_SCHEME_KRB_TICKET mentioned in other review comment

authenticationBytes = this.kerbTicket_.getClearCredential();
} else {
authenticationBytes = (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
}
}
catch (Exception e)
{
Expand Down Expand Up @@ -1838,6 +1849,8 @@ byte[] getPassword(byte[] clientSeed, byte[] serverSeed) throws AS400SecurityExc
if (credType == AS400.AUTHENTICATION_SCHEME_GSS_TOKEN)
{
try {
if (!kerbTicket_.isEmpty())
return kerbTicket_.getClearCredential();
return (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName_)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
Expand Down Expand Up @@ -2216,6 +2229,8 @@ private byte[] getDdmEncryptedPassword(byte[] sharedPrivateKey, byte[] serverSee
if (credType == AS400.AUTHENTICATION_SCHEME_GSS_TOKEN)
{
try {
if (!kerbTicket_.isEmpty())
return kerbTicket_.getClearCredential();
return (gssCredential_ == null)
? TokenManager.getGSSToken(systemName_, gssName_)
: TokenManager2.getGSSToken(systemName_, gssCredential_);
Expand Down Expand Up @@ -5402,4 +5417,10 @@ else if (profileToken.getTokenCreator() != ProfileTokenCredential.CREATOR_SIGNON
public String getLocalIPAddress() {
return localIPAddress_;
}

@Override
public void setKerbTicket(byte[] ticket) {
this.kerbTicket_ = new PasswordVault(ticket);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ public void close ()


as400_.disconnectServer (server_);

server_ = null;
}

Expand Down