![JAR search and dependency download from the Maven repository](/logo.png)
com.marklogic.xcc.impl.Credentials Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of marklogic-xcc Show documentation
Show all versions of marklogic-xcc Show documentation
MarkLogic XML Contentbase Connector for Java (XCC/J)
The newest version!
/*
* Copyright (c) 2024 MarkLogic Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.marklogic.xcc.impl;
import com.marklogic.io.Base64;
import com.marklogic.io.IOHelper;
import com.marklogic.xcc.UserCredentials;
import org.ietf.jgss.*;
import sun.security.krb5.KrbException;
import sun.security.krb5.PrincipalName;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Class Credentials used to be a static inner class of ContentSourceImpl.
* Pulling it out as a public class for refactoring purpose to support the
* new token-based authentication for MarkLogic Cloud.
*/
public class Credentials implements UserCredentials {
private static Random random = new Random();
private static final String DEFAULT_TOKEN_ENDPOINT = "/token";
private static final String DEFAULT_GRANT_TYPE = "apikey";
public static final int DEFAULT_TOKEN_DURATION = 60;
// For http basic and digest
private String user;
private char[] password;
private String basicAuth;
private String HA1;
// For http negotiate
private LoginContext loginContext;
// For mlcloud auth
private MLCloudAuthConfig mlCloudAuthConfig;
// For OAuth
private char[] OAuthToken;
public Credentials(char[] OAuthToken) {
this.OAuthToken = OAuthToken;
}
public Credentials(String user, char[] password) {
this.user = user;
this.password = password;
if (user != null && password != null) {
initBasicAuth();
}
}
public Credentials(char[] apiKey, String tokenEndpoint, String grantType,
int tokenDuration) {
mlCloudAuthConfig = new MLCloudAuthConfig(
apiKey, tokenEndpoint, grantType, tokenDuration);
}
public Credentials() {}
public String getUserName() {
return user;
}
public MLCloudAuthConfig getMLCloudAuthConfig() {
return mlCloudAuthConfig;
}
public char[] getOAuthToken() { return OAuthToken; }
void initBasicAuth(Charset encoding)
throws UnsupportedEncodingException {
byte[] ubytes = (user + ":").getBytes(encoding);
ByteBuffer pbuf = encoding.encode(CharBuffer.wrap(password));
byte[] upbytes = new byte[pbuf.remaining() + ubytes.length];
System.arraycopy(ubytes, 0, upbytes, 0, ubytes.length);
pbuf.get(upbytes, ubytes.length, pbuf.remaining());
basicAuth = "basic " +
Base64.encodeBytes(upbytes, Base64.DONT_BREAK_LINES);
}
void initBasicAuth() {
try {
initBasicAuth(StandardCharsets.UTF_8);
} catch (UnsupportedEncodingException e) {
try {
initBasicAuth(Charset.defaultCharset());
} catch (UnsupportedEncodingException e1) {
}
}
}
public String toHttpBasicAuth() {
if ((user == null) || ((password == null) && basicAuth == null)) {
throw new IllegalStateException("Invalid authentication credentials");
}
if (password != null) {
Arrays.fill(password, (char) 0);
password = null;
}
return basicAuth;
}
private static final AtomicLong nonceCounter = new AtomicLong();
public String toHttpDigestAuth(String method, String uri, String challengeHeader) {
if ((user == null) || (password == null && HA1 == null)) {
throw new IllegalStateException("Invalid authentication credentials");
}
if ((challengeHeader == null) || !challengeHeader.startsWith("Digest ")) {
return null;
}
String pairs[] = challengeHeader.substring("Digest ".length()).split(", +");
Map params = new HashMap<>();
for (String pair : pairs) {
String nv[] = pair.split("=", 2);
params.put(nv[0].toLowerCase(), nv[1].substring(1, nv[1].length() - 1));
}
String realm = params.get("realm");
if (HA1 == null) {
HA1 = digestCalcHA1(user, realm, password);
}
String nonce = params.get("nonce");
String qop = params.get("qop");
String opaque = params.get("opaque");
byte[] bytes = new byte[16];
synchronized (random) {
random.nextBytes(bytes);
}
String cNonce = IOHelper.bytesToHex(bytes);
String nonceCount = Long.toHexString(nonceCounter.incrementAndGet());
String response = digestCalcResponse(HA1, nonce, nonceCount, cNonce,
qop, method, uri);
StringBuilder buf = new StringBuilder();
buf.append("Digest username=\"");
buf.append(user);
buf.append("\", realm=\"");
buf.append(realm);
buf.append("\", nonce=\"");
buf.append(nonce);
buf.append("\", uri=\"");
buf.append(uri);
buf.append("\", qop=\"auth\", nc=\"");
buf.append(nonceCount);
buf.append("\", cnonce=\"");
buf.append(cNonce);
buf.append("\", response=\"");
buf.append(response);
buf.append("\", opaque=\"");
buf.append(opaque);
buf.append("\"");
return buf.toString();
}
public String toHttpNegotiateAuth(String hostName, String challenge) {
try {
if (loginContext == null)
buildSubjectCredentials();
return "Negotiate " + getAuthorizationHeader("HTTP/" + hostName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public String toMLCloudAuth() {
return "Bearer " + MLCloudAuthManager.getSessionToken(
this.getMLCloudAuthConfig().getApiKey());
}
public String toOAuth() {
return "Bearer " + new String(OAuthToken);
}
@Override
public String toString() {
return "user=" + user;
}
public static String digestCalcResponse(String HA1, String nonce,
String nonceCount, String cNonce,
String qop, String method,
String uri) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
StringBuilder plaintext = new StringBuilder();
plaintext.append(method);
plaintext.append(":");
plaintext.append(uri);
digest.update(plaintext.toString().getBytes(), 0, plaintext.length());
String HA2 = IOHelper.bytesToHex(digest.digest());
plaintext.setLength(0);
plaintext.append(HA1);
plaintext.append(":");
plaintext.append(nonce);
plaintext.append(":");
if (qop != null) {
plaintext.append(nonceCount);
plaintext.append(":");
plaintext.append(cNonce);
plaintext.append(":");
plaintext.append(qop);
plaintext.append(":");
}
plaintext.append(HA2);
digest.update(plaintext.toString().getBytes(), 0, plaintext.length());
return IOHelper.bytesToHex(digest.digest());
} catch (NoSuchAlgorithmException e) {
// this really shouldn't happen
throw new RuntimeException(e);
}
}
public static String digestCalcHA1(String userName, String realm,
char[] password) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
StringBuilder plaintext = new StringBuilder();
plaintext.append(userName);
plaintext.append(":");
plaintext.append(realm);
plaintext.append(":");
byte[] ubytes = plaintext.toString().getBytes();
ByteBuffer pbuf =
Charset.defaultCharset().encode(CharBuffer.wrap(password));
byte[] upbytes = new byte[pbuf.remaining() + ubytes.length];
System.arraycopy(ubytes, 0, upbytes, 0, ubytes.length);
pbuf.get(upbytes, ubytes.length, pbuf.remaining());
digest.update(upbytes, 0, upbytes.length);
return IOHelper.bytesToHex(digest.digest());
} catch (NoSuchAlgorithmException e) {
// this really shouldn't happen
throw new RuntimeException(e);
}
}
// -------------------------------------------------------------
/**
* Class to create Kerberos Configuration object which specifies the
* Kerberos Login Module to be used for authentication.
*/
private class KerberosLoginConfiguration extends Configuration {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map options = new HashMap<>();
options.put("refreshKrb5Config", "true");
options.put("useTicketCache", "true");
return new AppConfigurationEntry[]{
new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options)};
}
}
/**
* This method checks the validity of the TGT in the cache and build the
* Subject inside the LoginContext using Krb5LoginModule and the TGT cached
* by the Kerberos client. It assumes that a valid TGT is already present in
* the kerberos client's cache.
*
* @throws KrbException
* @throws IOException
* @throws LoginException
*/
private void buildSubjectCredentials()
throws KrbException, IOException, LoginException {
Subject subject = new Subject();
sun.security.krb5.Credentials cred;
// Check if the cache already has valid TGT information. If not, throw exceptions
if (user != null && !user.equals("")) {
cred = sun.security.krb5.Credentials.acquireTGTFromCache(
new PrincipalName(user), null);
} else {
cred = sun.security.krb5.Credentials.acquireTGTFromCache(null, null);
}
if (cred == null) {
throw new KrbException("No ticket granting ticket in the cache");
} else {
Date endTime = cred.getEndTime();
if (endTime != null) {
if (endTime.compareTo(new Date()) == -1) {
throw new KrbException("The ticket granting ticket in " +
"the cache is no longer valid");
}
}
}
/*
* We are not getting the TGT from KDC here. The actual TGT is got from
* the KDC using kinit or equivalent but we use the cached TGT in order
* to build the LoginContext and populate the TGT inside the Subject
* using Krb5LoginModule
*/
loginContext = new LoginContext("Krb5LoginContext", subject, null,
new KerberosLoginConfiguration());
loginContext.login();
}
/**
* Creates a privileged action which will be executed as the Subject using
* Subject.doAs() method. We do this in order to create a context of the
* user who has the service ticket and reuse this context for subsequent
* requests
*/
private static class CreateAuthorizationHeaderAction implements PrivilegedAction {
String clientPrincipalName;
String serverPrincipalName;
private StringBuffer outputToken = new StringBuffer();
private CreateAuthorizationHeaderAction(final String clientPrincipalName,
final String serverPrincipalName) {
this.clientPrincipalName = clientPrincipalName;
this.serverPrincipalName = serverPrincipalName;
}
private String getNegotiateToken() {
return outputToken.toString();
}
/*
* Here GSS API takes care of getting the service ticket from the Subject
* cache or by using the TGT information populated in the subject which is
* done by buildSubjectCredentials method. The service ticket received is
* populated in the subject's private credentials along with the TGT
* information since we will be executing this method as the Subject.
* For subsequent requests, the cached service ticket will be re-used.
* For this to work the System property
* javax.security.auth.useSubjectCredsOnly must be set to true.
*/
public Object run() {
try {
Oid krb5Mechanism = new Oid("1.2.840.113554.1.2.2");
Oid krb5PrincipalNameType = new Oid("1.2.840.113554.1.2.2.1");
final GSSManager manager = GSSManager.getInstance();
final GSSName clientName = manager.createName(clientPrincipalName, krb5PrincipalNameType);
final GSSCredential clientCred = manager.createCredential(clientName, 8 * 3600, krb5Mechanism,
GSSCredential.INITIATE_ONLY);
final GSSName serverName = manager.createName(serverPrincipalName, krb5PrincipalNameType);
final GSSContext context = manager.createContext(serverName, krb5Mechanism, clientCred,
GSSContext.DEFAULT_LIFETIME);
byte[] inToken = new byte[0]; // since
byte[] outToken = context.initSecContext(inToken, 0, inToken.length);
outputToken.append(new String(Base64.encodeBytes(outToken, Base64.DONT_BREAK_LINES)));
context.dispose();
} catch (GSSException exception) {
throw new RuntimeException(exception.getMessage());
}
return null;
}
}
/**
* This method builds the Authorization header for Kerberos. It
* generates a request token based on the service ticket, client principal name and
* time-stamp
*
* @param serverPrincipalName the name registered with the KDC of the service for which we
* need to authenticate
* @return the HTTP Authorization header token
*/
private String getAuthorizationHeader(String serverPrincipalName)
throws GSSException, LoginException, KrbException, IOException {
/*
* Get the principal from the Subject's private credentials and populate
* the client and server principal name for the GSS API
*/
final String clientPrincipal = getClientPrincipalName();
final CreateAuthorizationHeaderAction action =
new CreateAuthorizationHeaderAction(
clientPrincipal, serverPrincipalName);
/*
* Check if the TGT in the Subject's private credentials are valid. If
* valid, then we use the TGT in the Subject's private credentials. If
* not, we build the Subject's private credentials again from valid TGT
* in the Kerberos client cache.
*/
Set
© 2015 - 2025 Weber Informatics LLC | Privacy Policy