Skip to content

Commit

Permalink
fix fabric8io#4698: additional token refresh refinements
Browse files Browse the repository at this point in the history
openshift logic now functions more like the kube logic and will persist
in the kubeconfig, update the current config, and use the config refresh
logic to check for updated tokens.
  • Loading branch information
shawkins authored and manusa committed Jan 17, 2023
1 parent 905f8fd commit 5f3b68a
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,12 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S
return config;
}

/**
* Refresh the config from file / env sources.
* Any values that the user have programmatically set will be lost.
*
* @return
*/
public Config refresh() {
final String currentContextName = this.getCurrentContext() != null ? this.getCurrentContext().getName() : null;
if (this.autoConfigure) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class RequestConfig {

private String username;
private String password;
private String oauthToken;
private volatile String oauthToken;
private OAuthTokenProvider oauthTokenProvider;
private String impersonateUsername;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
package io.fabric8.kubernetes.client.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
import io.fabric8.kubernetes.client.http.HttpResponse;
Expand All @@ -25,7 +28,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStoreException;
Expand All @@ -37,6 +39,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
Expand Down Expand Up @@ -74,7 +77,8 @@ private OpenIDConnectionUtils() {
* @param currentAuthProviderConfig current AuthInfo's AuthProvider config as a map
* @return access token for interacting with Kubernetes API
*/
public static CompletableFuture<String> resolveOIDCTokenFromAuthConfig(Map<String, String> currentAuthProviderConfig,
public static CompletableFuture<String> resolveOIDCTokenFromAuthConfig(Config currentConfig,
Map<String, String> currentAuthProviderConfig,
HttpClient.Builder clientBuilder) {
String accessToken = currentAuthProviderConfig.get(ID_TOKEN_KUBECONFIG);
String issuer = currentAuthProviderConfig.get(ISSUER_KUBECONFIG);
Expand All @@ -84,32 +88,27 @@ public static CompletableFuture<String> resolveOIDCTokenFromAuthConfig(Map<Strin
String idpCert = currentAuthProviderConfig.get(IDP_CERT_DATA);
if (isTokenRefreshSupported(currentAuthProviderConfig)) {
return getOIDCProviderTokenEndpointAndRefreshToken(issuer, clientId, refreshToken, clientSecret, accessToken, idpCert,
clientBuilder);
clientBuilder).thenApply(map -> {
Object token = map.get(ID_TOKEN_PARAM);
if (token == null) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return accessToken;
}

// Persist new config and if successful, update the in memory config.
try {
persistKubeConfigWithUpdatedToken(currentConfig, map);
} catch (IOException e) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", e);
}

return String.valueOf(token);
});
}
return CompletableFuture.completedFuture(accessToken);
}

/**
* Get OIDC Provider discovery token_endpoint and issue refresh request
*
* @param client Http Client
* @param wellKnownOpenIdConfiguration OIDC Provider Discovery Document
* @param clientId client id as string
* @param refreshToken refresh token
* @param clientSecret client secret
* @param accessToken old access token
* @param shouldPersistUpdatedTokenInKubeConfig boolean value whether to modify kubeconfig file in disc or not
* @return returns access token(either updated or old) depending upon response from provider
*/
static CompletableFuture<String> getOIDCProviderTokenEndpointAndRefreshToken(HttpClient client,
Map<String, Object> wellKnownOpenIdConfiguration, String clientId, String refreshToken, String clientSecret,
String accessToken, boolean shouldPersistUpdatedTokenInKubeConfig) {
String oidcTokenEndpoint = getParametersFromDiscoveryResponse(wellKnownOpenIdConfiguration, TOKEN_ENDPOINT_PARAM);
CompletableFuture<String> freshAccessToken = OpenIDConnectionUtils.refreshToken(client, oidcTokenEndpoint, clientId,
refreshToken, clientSecret, shouldPersistUpdatedTokenInKubeConfig);
return freshAccessToken.thenApply(s -> Utils.getNonNullOrElse(s, accessToken));
}

/**
* Whether we should try to do token refresh or not, checks whether refresh-token key is set in
* HashMap or not
Expand All @@ -121,41 +120,6 @@ static boolean isTokenRefreshSupported(Map<String, String> currentAuthProviderCo
return Utils.isNotNull(currentAuthProviderConfig.get(REFRESH_TOKEN_KUBECONFIG));
}

/**
* Issue Token refresh request
*
* @param client http client
* @param oidcTokenEndpoint OIDC provider token endpoint
* @param clientId client id
* @param refreshToken refresh token for token refreshing
* @param clientSecret client secret
* @param shouldPersistUpdatedTokenInKubeConfig boolean value whether to update local kubeconfig file or not
* @return access token received from OpenID Connection provider
*/
static CompletableFuture<String> refreshToken(HttpClient client, String oidcTokenEndpoint, String clientId,
String refreshToken, String clientSecret, boolean shouldPersistUpdatedTokenInKubeConfig) {
CompletableFuture<Map<String, Object>> response = refreshOidcToken(client, clientId, refreshToken, clientSecret,
oidcTokenEndpoint);

return response.thenApply(map -> {
if (!map.containsKey(ID_TOKEN_PARAM)) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return null;
}

// Persist new config and if successful, update the in memory config.
try {
if (shouldPersistUpdatedTokenInKubeConfig && !persistKubeConfigWithUpdatedToken(map)) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG");
}
} catch (IOException e) {
LOGGER.warn("Failure in fetching refresh token: ", e);
}
return String.valueOf(map.get(ID_TOKEN_PARAM));
});
}

/**
* Issue Token Refresh HTTP Request to OIDC Provider
*
Expand Down Expand Up @@ -252,33 +216,50 @@ static String getParametersFromDiscoveryResponse(Map<String, Object> responseAsJ
/**
* Save Updated Access and Refresh token in local KubeConfig file.
*
* @param kubeConfigPath Path to KubeConfig (by default .kube/config)
* @param updatedAuthProviderConfig updated AuthProvider configuration
* @return boolean value whether update was successful not not
* @return boolean value whether update was successful or not
* @throws IOException in case of any failure while writing file
*/
static boolean persistKubeConfigWithUpdatedToken(String kubeConfigPath, Map<String, Object> updatedAuthProviderConfig)
static boolean persistKubeConfigWithUpdatedToken(Config currentConfig, Map<String, Object> updatedAuthProviderConfig)
throws IOException {
io.fabric8.kubernetes.api.model.Config config = KubeConfigUtils.parseConfig(new File(kubeConfigPath));
NamedContext currentNamedContext = KubeConfigUtils.getCurrentContext(config);

if (currentNamedContext != null) {
// Update users > auth-provider > config
int currentUserIndex = KubeConfigUtils.getNamedUserIndexFromConfig(config, currentNamedContext.getContext().getUser());
Map<String, String> authProviderConfig = config.getUsers().get(currentUserIndex).getUser().getAuthProvider().getConfig();
return persistKubeConfigWithUpdatedAuthInfo(currentConfig, a -> {
Map<String, String> authProviderConfig = a.getAuthProvider().getConfig();
authProviderConfig.put(ID_TOKEN_KUBECONFIG, String.valueOf(updatedAuthProviderConfig.get(ID_TOKEN_PARAM)));
authProviderConfig.put(REFRESH_TOKEN_KUBECONFIG, String.valueOf(updatedAuthProviderConfig.get(REFRESH_TOKEN_PARAM)));
config.getUsers().get(currentUserIndex).getUser().getAuthProvider().setConfig(authProviderConfig);
});
}

// Persist changes to KUBECONFIG
try {
KubeConfigUtils.persistKubeConfigIntoFile(config, kubeConfigPath);
return true;
} catch (IOException exception) {
LOGGER.warn("failed to write file {}", kubeConfigPath, exception);
}
/**
* Return true if the Config can be updated. false if not for a variety of reasons:
* - a kubeconfig file was not used
* - there's no current context
*/
public static boolean persistKubeConfigWithUpdatedAuthInfo(Config currentConfig, Consumer<AuthInfo> updateAction)
throws IOException {
if (currentConfig.getFile() == null) {
return false;
}
return false;
io.fabric8.kubernetes.api.model.Config config = KubeConfigUtils.parseConfig(currentConfig.getFile());
NamedContext currentNamedContext = currentConfig.getCurrentContext();

if (currentNamedContext == null) {
return false;
}
String userName = currentNamedContext.getContext().getUser();

NamedAuthInfo namedAuthInfo = config.getUsers().stream().filter(n -> n.getName().equals(userName)).findFirst()
.orElseGet(() -> {
NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo());
config.getUsers().add(result);
return result;
});
if (namedAuthInfo.getUser() == null) {
namedAuthInfo.setUser(new AuthInfo());
}
updateAction.accept(namedAuthInfo.getUser());
// Persist changes to KUBECONFIG
KubeConfigUtils.persistKubeConfigIntoFile(config, currentConfig.getFile().getAbsolutePath());
return true;
}

private static Map<String, Object> convertJsonStringToMap(String jsonString) throws JsonProcessingException {
Expand Down Expand Up @@ -325,19 +306,18 @@ private static Map<String, String> getRequestBodyContentForRefresh(String client
return result;
}

private static CompletableFuture<String> getOIDCProviderTokenEndpointAndRefreshToken(String issuer, String clientId,
String refreshToken, String clientSecret, String accessToken, String idpCert, HttpClient.Builder clientBuilder) {
private static CompletableFuture<Map<String, Object>> getOIDCProviderTokenEndpointAndRefreshToken(String issuer,
String clientId, String refreshToken, String clientSecret, String accessToken, String idpCert,
HttpClient.Builder clientBuilder) {
HttpClient newClient = getDefaultHttpClientWithPemCert(idpCert, clientBuilder);
CompletableFuture<Map<String, Object>> wellKnownOpenIdConfiguration = getOIDCDiscoveryDocumentAsMap(newClient, issuer);
CompletableFuture<String> result = wellKnownOpenIdConfiguration
.thenCompose(config -> getOIDCProviderTokenEndpointAndRefreshToken(newClient, config, clientId, refreshToken,
clientSecret, accessToken, true));
CompletableFuture<Map<String, Object>> result = getOIDCDiscoveryDocumentAsMap(newClient, issuer)
.thenCompose(wellKnownOpenIdConfiguration -> {
String oidcTokenEndpoint = getParametersFromDiscoveryResponse(wellKnownOpenIdConfiguration, TOKEN_ENDPOINT_PARAM);

return refreshOidcToken(newClient, clientId, refreshToken, clientSecret, oidcTokenEndpoint);
});
result.whenComplete((s, t) -> newClient.close());
return result;
}

private static boolean persistKubeConfigWithUpdatedToken(Map<String, Object> updatedAuthProviderConfig) throws IOException {
return persistKubeConfigWithUpdatedToken(io.fabric8.kubernetes.client.Config.getKubeconfigFilename(),
updatedAuthProviderConfig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private CompletableFuture<Boolean> refreshToken(BasicBuilder headerBuilder) {

private CompletableFuture<String> extractNewAccessTokenFrom(Config newestConfig) {
if (newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc")) {
return OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(newestConfig.getAuthProvider().getConfig(),
return OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(config, newestConfig.getAuthProvider().getConfig(),
factory.newBuilder());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
package io.fabric8.kubernetes.client.utils;

import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpResponse;
import io.fabric8.kubernetes.client.internal.KubeConfigUtils;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
Expand Down Expand Up @@ -120,7 +123,6 @@ void testFetchOIDCProviderDiscoveryDocumentAndRefreshToken() throws Exception {
// Given
Map<String, Object> discoveryDocument = new HashMap<>();
discoveryDocument.put(TOKEN_ENDPOINT_PARAM, "https://oauth2.exampleapis.com/token");
String accessToken = "some.access.token";
String clientId = "test-client-id";
String refreshToken = "test-refresh-token";
String clientSecret = "test-client-secret";
Expand All @@ -131,8 +133,10 @@ void testFetchOIDCProviderDiscoveryDocumentAndRefreshToken() throws Exception {
"\"token_type\": \"Bearer\"}");

// When
String newAccessToken = OpenIDConnectionUtils.getOIDCProviderTokenEndpointAndRefreshToken(mockClient,
discoveryDocument, clientId, refreshToken, clientSecret, accessToken, false).get();
String newAccessToken = String.valueOf(OpenIDConnectionUtils.refreshOidcToken(mockClient,
clientId, refreshToken, clientSecret,
OpenIDConnectionUtils.getParametersFromDiscoveryResponse(discoveryDocument, TOKEN_ENDPOINT_PARAM)).get()
.get(ID_TOKEN_PARAM));

// Then
assertNotNull(newAccessToken);
Expand All @@ -149,8 +153,11 @@ void testPersistKubeConfigWithUpdatedToken() throws IOException {
Files.copy(getClass().getResourceAsStream("/test-kubeconfig-oidc"), Paths.get(tempFile.getPath()),
StandardCopyOption.REPLACE_EXISTING);

Config theConfig = Config.fromKubeconfig(null, IOHelpers.readFully(new FileInputStream(tempFile), StandardCharsets.UTF_8),
tempFile.getAbsolutePath());

// When
boolean isPersisted = OpenIDConnectionUtils.persistKubeConfigWithUpdatedToken(tempFile.getAbsolutePath(),
boolean isPersisted = OpenIDConnectionUtils.persistKubeConfigWithUpdatedToken(theConfig,
openIdProviderResponse);

// Then
Expand All @@ -176,7 +183,7 @@ void testResolveOIDCTokenFromAuthConfigShouldReturnOldTokenWhenRefreshNotSupport
currentAuthProviderConfig.put(ID_TOKEN_KUBECONFIG, "id-token");

// When
String token = OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(currentAuthProviderConfig, null).get();
String token = OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(Config.empty(), currentAuthProviderConfig, null).get();

// Then
assertEquals("id-token", token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@
import java.text.ParseException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

/**
Expand Down Expand Up @@ -703,7 +702,7 @@ protected void setDerivedFields() {
HttpClient.DerivedClientBuilder builder = httpClient.newBuilder().authenticatorNone();
this.httpClient = builder
.addOrReplaceInterceptor(TokenRefreshInterceptor.NAME,
new OpenShiftOAuthInterceptor(httpClient, wrapped, new AtomicReference<>()))
new OpenShiftOAuthInterceptor(httpClient, wrapped))
.build();
try {
this.openShiftUrl = new URL(wrapped.getOpenShiftUrl());
Expand Down

0 comments on commit 5f3b68a

Please sign in to comment.