Skip to content

Commit

Permalink
fix (kubernetes-client-api) : Add new autoOAuthToken field in Config …
Browse files Browse the repository at this point in the history
…to differentiate between user and autoconfigured tokens (fabric8io#4802)

+ Add a new string field authOAuthToken in Config, this field would be
  used by Config internally to store token obtained during
  autoconfiguration process.
+ Remove setOAuthToken references from token interceptors and Config, it
  would only be called by user (during `new ConfigBuilder().withOAuthToken("...")` call)
  whenever custom token is required.
+ getOAuthToken would give most priority to token from provider, then
  actual value of `oauthToken` and finally `autoOAuthToken` value. All
  interceptors would keep using getOAuthToken to get resolved OAuth
  token value

Signed-off-by: Rohan Kumar <rohaan@redhat.com>
  • Loading branch information
rohanKanojia committed Mar 10, 2023
1 parent ddfab72 commit e453068
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 87 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 6.6-SNAPSHOT

#### Bugs
* Fix #4802: config.refresh() erases token specified when building initial config

#### Improvements

Expand Down Expand Up @@ -1925,3 +1926,4 @@ like the delete of a custom resource.
* Fixed issue of SecurityContextConstraints not working - https://github.com/fabric8io/kubernetes-client/pull/982
Note :- This got fixed by fixing model - https://github.com/fabric8io/kubernetes-model/pull/274
Dependencies Upgrade

Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public class Config {
private String username;
private String password;
private volatile String oauthToken;
private volatile String autoOAuthToken;
private OAuthTokenProvider oauthTokenProvider;
private long websocketPingInterval = DEFAULT_WEBSOCKET_PING_INTERVAL;
private int connectionTimeout = 10 * 1000;
Expand Down Expand Up @@ -318,14 +319,14 @@ private static String ensureHttps(String masterUrl, Config config) {
public Config(String masterUrl, String apiVersion, String namespace, boolean trustCerts, boolean disableHostnameVerification,
String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile,
String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password,
String oauthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, int requestTimeout,
String oauthToken, String autoOAuthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, int requestTimeout,
long rollingTimeout, long scaleTimeout, int loggingInterval, int maxConcurrentRequests, int maxConcurrentRequestsPerHost,
String httpProxy, String httpsProxy, String[] noProxy, Map<Integer, String> errorMessages, String userAgent,
TlsVersion[] tlsVersions, long websocketTimeout, long websocketPingInterval, String proxyUsername, String proxyPassword,
String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase,
String impersonateUsername, String[] impersonateGroups, Map<String, List<String>> impersonateExtras) {
this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, clientCertFile,
clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, oauthToken,
clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, oauthToken, autoOAuthToken,
watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, scaleTimeout,
loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, false, httpProxy, httpsProxy, noProxy,
errorMessages, userAgent, tlsVersions, websocketTimeout, websocketPingInterval, proxyUsername, proxyPassword,
Expand All @@ -338,7 +339,7 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru
public Config(String masterUrl, String apiVersion, String namespace, boolean trustCerts, boolean disableHostnameVerification,
String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile,
String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password,
String oauthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, int requestTimeout,
String oauthToken, String autoOAuthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, int requestTimeout,
long scaleTimeout, int loggingInterval, int maxConcurrentRequests, int maxConcurrentRequestsPerHost,
boolean http2Disable, String httpProxy, String httpsProxy, String[] noProxy, Map<Integer, String> errorMessages,
String userAgent, TlsVersion[] tlsVersions, long websocketTimeout, long websocketPingInterval, String proxyUsername,
Expand Down Expand Up @@ -426,7 +427,7 @@ public static void configFromSysPropsOrEnvVars(Config config) {
Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_PASSPHRASE_PROPERTY, config.getKeyStorePassphrase()));
config.setKeyStoreFile(Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_FILE_PROPERTY, config.getKeyStoreFile()));

config.setOauthToken(Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY, config.getOauthToken()));
config.setAutoOAuthToken(Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY, config.getAutoOAuthToken()));
config.setUsername(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_USERNAME_SYSTEM_PROPERTY, config.getUsername()));
config.setPassword(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_PASSWORD_SYSTEM_PROPERTY, config.getPassword()));

Expand Down Expand Up @@ -551,7 +552,7 @@ private static boolean tryServiceAccount(Config config) {
try {
String serviceTokenCandidate = new String(Files.readAllBytes(saTokenPathFile.toPath()));
LOGGER.debug("Found service account token at: [{}].", saTokenPathLocation);
config.setOauthToken(serviceTokenCandidate);
config.setAutoOAuthToken(serviceTokenCandidate);
String txt = "Configured service account doesn't have access. Service account may have been revoked.";
config.getErrorMessages().put(401, "Unauthorized! " + txt);
config.getErrorMessages().put(403, "Forbidden!" + txt);
Expand Down Expand Up @@ -633,6 +634,9 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S
*/
public Config refresh() {
final String currentContextName = this.getCurrentContext() != null ? this.getCurrentContext().getName() : null;
if (this.oauthToken != null && !this.oauthToken.isEmpty()) {
return this;
}
if (this.autoConfigure) {
return Config.autoConfigure(currentContextName);
}
Expand Down Expand Up @@ -730,27 +734,27 @@ private static boolean loadFromKubeconfig(Config config, String context, String
config.setClientKeyFile(clientKeyFile);
config.setClientKeyData(currentAuthInfo.getClientKeyData());
config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData()));
config.setOauthToken(currentAuthInfo.getToken());
config.setAutoOAuthToken(currentAuthInfo.getToken());
config.setUsername(currentAuthInfo.getUsername());
config.setPassword(currentAuthInfo.getPassword());

if (Utils.isNullOrEmpty(config.getOauthToken()) && currentAuthInfo.getAuthProvider() != null) {
if (Utils.isNullOrEmpty(config.getAutoOAuthToken()) && currentAuthInfo.getAuthProvider() != null) {
if (currentAuthInfo.getAuthProvider().getConfig() != null) {
config.setAuthProvider(currentAuthInfo.getAuthProvider());
if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN))) {
// GKE token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN));
config.setAutoOAuthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN));
} else if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN))) {
// OpenID Connect token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN));
config.setAutoOAuthToken(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN));
}
}
} else if (config.getOauthTokenProvider() == null) { // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
ExecConfig exec = currentAuthInfo.getExec();
if (exec != null) {
ExecCredential ec = getExecCredentialFromExecConfig(exec, configFile);
if (ec != null && ec.status != null && ec.status.token != null) {
config.setOauthToken(ec.status.token);
config.setAutoOAuthToken(ec.status.token);
} else {
LOGGER.warn("No token returned");
}
Expand Down Expand Up @@ -976,7 +980,10 @@ public String getOauthToken() {
if (this.oauthTokenProvider != null) {
return this.oauthTokenProvider.getToken();
}
return oauthToken;
if (this.oauthToken != null) {
return oauthToken;
}
return autoOAuthToken;
}

public void setOauthToken(String oauthToken) {
Expand Down Expand Up @@ -1489,4 +1496,12 @@ public void setAutoConfigure(boolean autoConfigure) {
this.autoConfigure = autoConfigure;
}

public String getAutoOAuthToken() {
return autoOAuthToken;
}

public void setAutoOAuthToken(String autoOAuthToken) {
this.autoOAuthToken = autoOAuthToken;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
Expand Down Expand Up @@ -66,6 +68,9 @@ public class OpenIDConnectionUtils {
public static final String TOKEN_ENDPOINT_PARAM = "token_endpoint";
public static final String WELL_KNOWN_OPENID_CONFIGURATION = ".well-known/openid-configuration";
public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
private static final String JWT_TOKEN_EXPIRY_TIMESTAMP_KEY = "exp";
public static final String JWT_PARTS_DELIMITER_REGEX = "\\.";
private static final int TOKEN_EXPIRY_DELTA = 10;

private OpenIDConnectionUtils() {
}
Expand Down Expand Up @@ -319,4 +324,33 @@ private static CompletableFuture<Map<String, Object>> getOIDCProviderTokenEndpoi
return result;
}

public static boolean idTokenExpired(Config config) {
if (config.getAuthProvider() != null && config.getAuthProvider().getConfig() != null) {
Map<String, String> authProviderConfig = config.getAuthProvider().getConfig();
String accessToken = authProviderConfig.get(ID_TOKEN_KUBECONFIG);
if (isValidJwt(accessToken)) {
try {
String[] jwtParts = accessToken.split(JWT_PARTS_DELIMITER_REGEX);
String jwtPayload = jwtParts[1];
String jwtPayloadDecoded = new String(Base64.getDecoder().decode(jwtPayload));
Map<String, Object> jwtPayloadMap = Serialization.jsonMapper().readValue(jwtPayloadDecoded, Map.class);
int expiryTimestampInSeconds = (Integer) jwtPayloadMap.get(JWT_TOKEN_EXPIRY_TIMESTAMP_KEY);
return Instant.ofEpochSecond(expiryTimestampInSeconds)
.minusSeconds(TOKEN_EXPIRY_DELTA)
.isBefore(Instant.now());
} catch (JsonProcessingException e) {
return true;
}
}
}
return true;
}

private static boolean isValidJwt(String token) {
if (token != null && !token.isEmpty()) {
String[] jwtParts = token.split(JWT_PARTS_DELIMITER_REGEX);
return jwtParts.length == 3;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public CompletableFuture<Boolean> afterFailure(BasicBuilder headerBuilder, HttpR
if (isBasicAuth()) {
return CompletableFuture.completedFuture(false);
}
if (config.getOauthTokenProvider() != null) {
String tokenFromProvider = config.getOauthTokenProvider().getToken();
if (tokenFromProvider != null && !tokenFromProvider.isEmpty()) {
return CompletableFuture.completedFuture(overrideNewAccessTokenToConfig(tokenFromProvider, headerBuilder, config));
}
}
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
return refreshToken(headerBuilder);
}
Expand All @@ -91,7 +97,7 @@ private CompletableFuture<Boolean> refreshToken(BasicBuilder headerBuilder) {
}

private CompletableFuture<String> extractNewAccessTokenFrom(Config newestConfig) {
if (newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc")) {
if (isAuthProviderOidc(newestConfig) && OpenIDConnectionUtils.idTokenExpired(newestConfig)) {
return OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(config, newestConfig.getAuthProvider().getConfig(),
factory.newBuilder());
}
Expand All @@ -102,7 +108,7 @@ private CompletableFuture<String> extractNewAccessTokenFrom(Config newestConfig)
private boolean overrideNewAccessTokenToConfig(String newAccessToken, BasicBuilder headerBuilder, Config existConfig) {
if (Utils.isNotNullOrEmpty(newAccessToken)) {
headerBuilder.setHeader(AUTHORIZATION, "Bearer " + newAccessToken);
existConfig.setOauthToken(newAccessToken);
existConfig.setAutoOAuthToken(newAccessToken);

updateLatestRefreshTimestamp();

Expand All @@ -116,4 +122,7 @@ private void updateLatestRefreshTimestamp() {
latestRefreshTimestamp = Instant.now();
}

private static boolean isAuthProviderOidc(Config newestConfig) {
return newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
Expand All @@ -49,7 +50,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;

public class ConfigTest {
class ConfigTest {

private static final String TEST_KUBECONFIG_FILE = Utils.filePath(ConfigTest.class.getResource("/test-kubeconfig"));
private static final String TEST_EC_KUBECONFIG_FILE = Utils.filePath(ConfigTest.class.getResource("/test-ec-kubeconfig"));
Expand Down Expand Up @@ -578,31 +579,31 @@ void testEmptyConfig() {
emptyConfig = Config.empty();

// Then
assertNotNull(emptyConfig);
assertEquals("https://kubernetes.default.svc", emptyConfig.getMasterUrl());
assertTrue(emptyConfig.getContexts().isEmpty());
assertNull(emptyConfig.getCurrentContext());
assertEquals(64, emptyConfig.getMaxConcurrentRequests());
assertEquals(5, emptyConfig.getMaxConcurrentRequestsPerHost());
assertFalse(emptyConfig.isTrustCerts());
assertFalse(emptyConfig.isDisableHostnameVerification());
assertEquals("RSA", emptyConfig.getClientKeyAlgo());
assertEquals("changeit", emptyConfig.getClientKeyPassphrase());
assertEquals(1000, emptyConfig.getWatchReconnectInterval());
assertEquals(-1, emptyConfig.getWatchReconnectLimit());
assertEquals(10000, emptyConfig.getConnectionTimeout());
assertEquals(10000, emptyConfig.getRequestTimeout());
assertEquals(600000, emptyConfig.getScaleTimeout());
assertEquals(20000, emptyConfig.getLoggingInterval());
assertEquals(5000, emptyConfig.getWebsocketTimeout());
assertEquals(30000, emptyConfig.getWebsocketPingInterval());
assertEquals(120000, emptyConfig.getUploadRequestTimeout());
assertTrue(emptyConfig.getImpersonateExtras().isEmpty());
assertEquals(0, emptyConfig.getImpersonateGroups().length);
assertFalse(emptyConfig.isHttp2Disable());
assertEquals(1, emptyConfig.getTlsVersions().length);
assertTrue(emptyConfig.getErrorMessages().isEmpty());
assertNotNull(emptyConfig.getUserAgent());
assertThat(emptyConfig)
.hasFieldOrPropertyWithValue("masterUrl", "https://kubernetes.default.svc")
.hasFieldOrPropertyWithValue("contexts", Collections.emptyList())
.hasFieldOrPropertyWithValue("maxConcurrentRequests", 64)
.hasFieldOrPropertyWithValue("maxConcurrentRequestsPerHost", 5)
.hasFieldOrPropertyWithValue("trustCerts", false)
.hasFieldOrPropertyWithValue("disableHostnameVerification", false)
.hasFieldOrPropertyWithValue("clientKeyAlgo", "RSA")
.hasFieldOrPropertyWithValue("clientKeyPassphrase", "changeit")
.hasFieldOrPropertyWithValue("watchReconnectInterval", 1000)
.hasFieldOrPropertyWithValue("watchReconnectLimit", -1)
.hasFieldOrPropertyWithValue("connectionTimeout", 10000)
.hasFieldOrPropertyWithValue("requestTimeout", 10000)
.hasFieldOrPropertyWithValue("scaleTimeout", 600000L)
.hasFieldOrPropertyWithValue("loggingInterval", 20000)
.hasFieldOrPropertyWithValue("websocketTimeout", 5000L)
.hasFieldOrPropertyWithValue("websocketPingInterval", 30000L)
.hasFieldOrPropertyWithValue("uploadRequestTimeout", 120000)
.hasFieldOrPropertyWithValue("impersonateExtras", Collections.emptyMap())
.hasFieldOrPropertyWithValue("http2Disable", false)
.hasFieldOrPropertyWithValue("tlsVersions", new TlsVersion[] { TlsVersion.TLS_1_2 })
.hasFieldOrPropertyWithValue("errorMessages", Collections.emptyMap())
.satisfies(e -> assertThat(e.getCurrentContext()).isNull())
.satisfies(e -> assertThat(e.getImpersonateGroups()).isEmpty())
.satisfies(e -> assertThat(e.getUserAgent()).isNotNull());
}

private void assertConfig(Config config) {
Expand Down Expand Up @@ -802,4 +803,20 @@ void getHomeDir_shouldReturnUserHomeProp_WhenHomeEnvVariablesAreNotSet() {
System.setProperty("user.home", userHomePropToRestore);
}
}

@Test
void refresh_whenOAuthTokenSourceSetToUser_thenConfigUnchanged() {
// Given
Config config = new ConfigBuilder()
.withOauthToken("token-from-user")
.build();

// When
Config updatedConfig = config.refresh();

// Then
assertThat(updatedConfig)
.isSameAs(config)
.hasFieldOrPropertyWithValue("oauthToken", "token-from-user");
}
}
Loading

0 comments on commit e453068

Please sign in to comment.