diff --git a/CHANGELOG.md b/CHANGELOG.md index cc457cfa637..525d260e7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 6.6-SNAPSHOT #### Bugs +* Fix #4802: config.refresh() erases token specified when building initial config * Fix #4963: Openshift Client return 403 when use websocket * Fix #4985: triggering the immediate cleanup of the okhttp idle task * fix #5002: Jetty response completion accounts for header processing @@ -1941,3 +1942,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 + diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java index f1c3f2889e7..89cd08b80c8 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java @@ -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; @@ -318,7 +319,8 @@ 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 errorMessages, String userAgent, TlsVersion[] tlsVersions, long websocketTimeout, long websocketPingInterval, String proxyUsername, String proxyPassword, @@ -326,6 +328,7 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras) { this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, clientCertFile, 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, @@ -338,7 +341,8 @@ 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 errorMessages, String userAgent, TlsVersion[] tlsVersions, long websocketTimeout, long websocketPingInterval, String proxyUsername, @@ -426,7 +430,8 @@ 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())); @@ -551,7 +556,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); @@ -633,6 +638,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); } @@ -730,19 +738,19 @@ 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 @@ -750,7 +758,7 @@ private static boolean loadFromKubeconfig(Config config, String context, String 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"); } @@ -976,7 +984,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) { @@ -1489,4 +1500,12 @@ public void setAutoConfigure(boolean autoConfigure) { this.autoConfigure = autoConfigure; } + public String getAutoOAuthToken() { + return autoOAuthToken; + } + + public void setAutoOAuthToken(String autoOAuthToken) { + this.autoOAuthToken = autoOAuthToken; + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java index cf9ce5452e7..1f39a0343a7 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java @@ -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; @@ -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() { } @@ -319,4 +324,33 @@ private static CompletableFuture> getOIDCProviderTokenEndpoi return result; } + public static boolean idTokenExpired(Config config) { + if (config.getAuthProvider() != null && config.getAuthProvider().getConfig() != null) { + Map 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 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; + } } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptor.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptor.java index 78311265138..baa2c8f7c72 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptor.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptor.java @@ -77,6 +77,12 @@ public CompletableFuture 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); } @@ -91,7 +97,7 @@ private CompletableFuture refreshToken(BasicBuilder headerBuilder) { } private CompletableFuture 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()); } @@ -102,7 +108,7 @@ private CompletableFuture 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(); @@ -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"); + } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java index ffde9029b01..13048adc6bf 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java @@ -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; @@ -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")); @@ -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) { @@ -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"); + } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java index 95f10718f22..afc04a98da8 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java @@ -15,6 +15,7 @@ */ package io.fabric8.kubernetes.client.utils; +import io.fabric8.kubernetes.api.model.AuthProviderConfigBuilder; import io.fabric8.kubernetes.api.model.NamedContext; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; @@ -34,6 +35,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -46,6 +49,7 @@ import static io.fabric8.kubernetes.client.utils.OpenIDConnectionUtils.REFRESH_TOKEN_KUBECONFIG; import static io.fabric8.kubernetes.client.utils.OpenIDConnectionUtils.REFRESH_TOKEN_PARAM; import static io.fabric8.kubernetes.client.utils.OpenIDConnectionUtils.TOKEN_ENDPOINT_PARAM; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -239,6 +243,42 @@ void testgetParametersFromDiscoveryResponse() { assertEquals("", OpenIDConnectionUtils.getParametersFromDiscoveryResponse(discoveryDocument, "userinfo_endpoint")); } + @Test + void idTokenExpired_whenEmptyFormatProvided_thenReturnTrue() { + assertThat(OpenIDConnectionUtils.idTokenExpired(createNewConfigWithAuthProviderIdToken(""))).isTrue(); + } + + @Test + void idTokenExpired_whenInvalidJwtTokenFormatProvided_thenReturnTrue() { + assertThat(OpenIDConnectionUtils.idTokenExpired(createNewConfigWithAuthProviderIdToken("invalid-jwt-token"))).isTrue(); + } + + @Test + void idTokenExpired_whenInvalidJwtPayloadProvided_thenReturnTrue() { + assertThat(OpenIDConnectionUtils.idTokenExpired(createNewConfigWithAuthProviderIdToken("header.payload.signature"))) + .isTrue(); + } + + @Test + void idTokenExpired_whenOldTokenProvided_thenReturnTrue() { + // Given + String token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL21sYi50cmVtb2xvLmxhbjo4MDQzL2F1dGgvaWRwL29pZGMiLCJhdWQiOiJrdWJlcm5ldGVzIiwiZXhwIjoxNDc0NTk2NjY5LCJqdGkiOiI2RDUzNXoxUEpFNjJOR3QxaWVyYm9RIiwiaWF0IjoxNDc0NTk2MzY5LCJuYmYiOjE0NzQ1OTYyNDksInN1YiI6Im13aW5kdSIsInVzZXJfcm9sZSI6WyJ1c2VycyIsIm5ldy1uYW1lc3BhY2Utdmlld2VyIl0sImVtYWlsIjoibXdpbmR1QG5vbW9yZWplZGkuY29tIn0.f2As579n9VNoaKzoF-dOQGmXkFKf1FMyNV0-va_B63jn-_n9LGSCca_6IVMP8pO-Zb4KvRqGyTP0r3HkHxYy5c81AnIh8ijarruczl-TK_yF5akjSTHFZD-0gRzlevBDiH8Q79NAr-ky0P4iIXS8lY9Vnjch5MF74Zx0c3alKJHJUnnpjIACByfF2SCaYzbWFMUNat-K1PaUk5-ujMBG7yYnr95xD-63n8CO8teGUAAEMx6zRjzfhnhbzX-ajwZLGwGUBT4WqjMs70-6a7_8gZmLZb2az1cZynkFRj2BaCkVT3A2RrjeEwZEtGXlMqKJ1_I2ulrOVsYx01_yD35-rw"; + + // When + Then + assertThat(OpenIDConnectionUtils.idTokenExpired(createNewConfigWithAuthProviderIdToken(token))).isTrue(); + } + + @Test + void idTokenExpired_whenTokenStillNotExpired_thenReturnFalse() { + // Given + Instant tokenExp = Instant.now().plusSeconds(30); + String payload = "{\"exp\": " + tokenExp.getEpochSecond() + "}"; + String token = "header." + Base64.getEncoder().encodeToString(payload.getBytes()) + ".signature"; + + // When + Then + assertThat(OpenIDConnectionUtils.idTokenExpired(createNewConfigWithAuthProviderIdToken(token))).isFalse(); + } + private void mockHttpClient(int responseCode, String responseAsStr) throws IOException { HttpResponse mockSuccessResponse = mockResponse(responseCode, responseAsStr); when(mockClient.sendAsync(any(), eq(String.class))) @@ -251,4 +291,12 @@ private HttpResponse mockResponse(int responseCode, String responseBody) Mockito.when(response.body()).thenReturn(responseBody); return response; } + + private Config createNewConfigWithAuthProviderIdToken(String idToken) { + return new ConfigBuilder(Config.empty()) + .withAuthProvider(new AuthProviderConfigBuilder() + .addToConfig(ID_TOKEN_KUBECONFIG, idToken) + .build()) + .build(); + } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptorTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptorTest.java index fc895099109..e50deac5aba 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptorTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/TokenRefreshInterceptorTest.java @@ -32,12 +32,15 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import static io.fabric8.kubernetes.client.Config.KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY; import static io.fabric8.kubernetes.client.Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY; import static io.fabric8.kubernetes.client.Config.KUBERNETES_KUBECONFIG_FILE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -55,7 +58,7 @@ void shouldAutoconfigureAfter401() throws Exception { Paths.get(tempFile.getPath()), StandardCopyOption.REPLACE_EXISTING); System.setProperty(KUBERNETES_KUBECONFIG_FILE, tempFile.getAbsolutePath()); - HttpRequest.Builder builder = Mockito.mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); // Call boolean reissue = new TokenRefreshInterceptor(Config.autoConfigure(null), null, Instant.now()) @@ -77,7 +80,7 @@ void shouldAutoconfigureAfter1Minute() throws Exception { Paths.get(tempFile.getPath()), StandardCopyOption.REPLACE_EXISTING); System.setProperty(KUBERNETES_KUBECONFIG_FILE, tempFile.getAbsolutePath()); - HttpRequest.Builder builder = Mockito.mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); // Call TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor(Config.autoConfigure(null), @@ -117,16 +120,12 @@ void refreshShouldNotOverwriteExistingToken() throws Exception { } @Test - @DisplayName("#4442 token auto refresh should overwrite existing token when applicable") - void refreshShouldOverwriteExistingToken() throws Exception { + @DisplayName("#4442 token auto refresh should not overwrite existing token when provided by user") + void refresh_whenNoAuthProvider_thenShouldInheritTokenFromOldConfig() throws Exception { // Given - final Config originalConfig = spy(new ConfigBuilder(Config.empty()) + final Config originalConfig = new ConfigBuilder(Config.empty()) .withOauthToken("existing-token") - .build()); - final Config autoConfig = new ConfigBuilder(Config.empty()) - .withOauthToken("new-token") .build(); - when(originalConfig.refresh()).thenReturn(autoConfig); final TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor( originalConfig, null, Instant.now().minusSeconds(61)); // When @@ -134,7 +133,7 @@ void refreshShouldOverwriteExistingToken() throws Exception { .afterFailure(new StandardHttpRequest.Builder(), new TestHttpResponse<>().withCode(401), null).get(); // Then assertThat(result).isTrue(); - assertThat(originalConfig).hasFieldOrPropertyWithValue("oauthToken", "new-token"); + assertThat(originalConfig).hasFieldOrPropertyWithValue("oauthToken", "existing-token"); } @Test @@ -147,7 +146,7 @@ void shouldReloadInClusterServiceAccount() throws Exception { System.setProperty(KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY, tokenFile.getAbsolutePath()); System.setProperty(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); - HttpRequest.Builder builder = Mockito.mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); // The expired token will be read at auto configure. TokenRefreshInterceptor interceptor = new TokenRefreshInterceptor(Config.autoConfigure(null), null, Instant.now()); @@ -176,7 +175,7 @@ void shouldRefreshOIDCToken() throws Exception { System.setProperty(KUBERNETES_KUBECONFIG_FILE, tempFile.getAbsolutePath()); // Prepare HTTP call that will fail with 401 Unauthorized to trigger OIDC token renewal. - HttpRequest.Builder builder = Mockito.mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); // Loads the initial kubeconfig, including initial token value. Config config = Config.autoConfigure(null); @@ -190,7 +189,7 @@ void shouldRefreshOIDCToken() throws Exception { Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/token-refresh-interceptor/kubeconfig-oidc")), Paths.get(tempFile.getPath()), StandardCopyOption.REPLACE_EXISTING); - TokenRefreshInterceptor interceptor = new TokenRefreshInterceptor(config, Mockito.mock(HttpClient.Factory.class), + TokenRefreshInterceptor interceptor = new TokenRefreshInterceptor(config, mock(HttpClient.Factory.class), Instant.now()); boolean reissue = interceptor.afterFailure(builder, new TestHttpResponse<>().withCode(401), null).get(); @@ -203,4 +202,72 @@ void shouldRefreshOIDCToken() throws Exception { } } + + @Test + void afterFailure_whenTokenUpdatedPostRefreshUsingExecCredentials_thenUseUpdatedToken() + throws ExecutionException, InterruptedException { + // Given + final Config oldConfig = mock(Config.class); + final Config newConfig = mock(Config.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + when(oldConfig.refresh()).thenReturn(newConfig); + when(newConfig.getOauthToken()).thenReturn("token-from-exec-credentials"); + final TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor( + oldConfig, null, Instant.now().minusSeconds(61)); + // When + final boolean result = tokenRefreshInterceptor + .afterFailure(builder, new TestHttpResponse<>().withCode(401), null).get(); + // Then + assertThat(result).isTrue(); + Mockito.verify(builder).setHeader("Authorization", "Bearer token-from-exec-credentials"); + } + + @Test + void afterFailure_whenTokenFromOAuthTokenProvider_thenUseUpdatedToken() throws ExecutionException, InterruptedException { + // Given + final Config oldConfig = mock(Config.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + when(oldConfig.getOauthTokenProvider()).thenReturn(() -> "token-from-oauthtokenprovider"); + final TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor( + oldConfig, null, Instant.now().minusSeconds(61)); + // When + final boolean result = tokenRefreshInterceptor + .afterFailure(builder, new TestHttpResponse<>().withCode(401), null).get(); + // Then + assertThat(result).isTrue(); + Mockito.verify(builder).setHeader("Authorization", "Bearer token-from-oauthtokenprovider"); + } + + @Test + void afterFailure_whenBasicAuth_thenCompleteWithFalse() { + // Given + final Config config = mock(Config.class); + when(config.getUsername()).thenReturn("kubeadmin"); + when(config.getPassword()).thenReturn("secret"); + final TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor( + config, null, Instant.now().minusSeconds(61)); + + // When + CompletableFuture result = tokenRefreshInterceptor.afterFailure(null, null, null); + + // Then + assertThat(result).isCompletedWithValue(false); + } + + @Test + void before_whenBasicAuth_thenUseCredentialsInHeader() { + // Given + final Config config = mock(Config.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + when(config.getUsername()).thenReturn("kubeadmin"); + when(config.getPassword()).thenReturn("secret"); + final TokenRefreshInterceptor tokenRefreshInterceptor = new TokenRefreshInterceptor( + config, null, Instant.now().minusSeconds(61)); + + // When + tokenRefreshInterceptor.before(builder, null, null); + + // Then + Mockito.verify(builder).header("Authorization", HttpClientUtils.basicCredentials("kubeadmin", "secret")); + } } diff --git a/openshift-client-api/src/main/java/io/fabric8/openshift/client/OpenShiftConfig.java b/openshift-client-api/src/main/java/io/fabric8/openshift/client/OpenShiftConfig.java index bd4daba4d2e..6fd46874756 100644 --- a/openshift-client-api/src/main/java/io/fabric8/openshift/client/OpenShiftConfig.java +++ b/openshift-client-api/src/main/java/io/fabric8/openshift/client/OpenShiftConfig.java @@ -76,7 +76,8 @@ public OpenShiftConfig(String openShiftUrl, String oapiVersion, String masterUrl String clientCertFile, String clientCertData, String clientKeyFile, String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, - String username, String password, String oauthToken, int watchReconnectInterval, int watchReconnectLimit, + String username, String password, 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, @@ -91,13 +92,15 @@ public OpenShiftConfig(String openShiftUrl, String oapiVersion, String masterUrl super(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, - oauthToken, + oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, http2Disable, httpProxy, httpsProxy, noProxy, errorMessages, userAgent, tlsVersions, websocketTimeout, websocketPingInterval, proxyUsername, proxyPassword, trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups, - impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval, + impersonateExtras, oauthTokenProvider, customHeaders, + requestRetryBackoffLimit, + requestRetryBackoffInterval, uploadRequestTimeout); this.setOapiVersion(oapiVersion); this.setBuildTimeout(buildTimeout); @@ -122,7 +125,8 @@ public OpenShiftConfig(Config kubernetesConfig, String openShiftUrl, String oapi kubernetesConfig.getClientKeyData(), kubernetesConfig.getClientKeyAlgo(), kubernetesConfig.getClientKeyPassphrase(), kubernetesConfig.getUsername(), kubernetesConfig.getPassword(), kubernetesConfig.getOauthToken(), - kubernetesConfig.getWatchReconnectInterval(), kubernetesConfig.getWatchReconnectLimit(), + kubernetesConfig.getAutoOAuthToken(), kubernetesConfig.getWatchReconnectInterval(), + kubernetesConfig.getWatchReconnectLimit(), kubernetesConfig.getConnectionTimeout(), kubernetesConfig.getRequestTimeout(), kubernetesConfig.getScaleTimeout(), kubernetesConfig.getLoggingInterval(), kubernetesConfig.getMaxConcurrentRequests(), diff --git a/openshift-client/src/main/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptor.java b/openshift-client/src/main/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptor.java index 81f073ef5bb..783c8bc7f40 100644 --- a/openshift-client/src/main/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptor.java +++ b/openshift-client/src/main/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptor.java @@ -92,6 +92,12 @@ public void before(BasicBuilder builder, HttpRequest httpRequest, RequestTags ta @Override public CompletableFuture afterFailure(BasicBuilder builder, HttpResponse response, RequestTags tags) { + if (config.getOauthTokenProvider() != null) { + String tokenFromProvider = config.getOauthTokenProvider().getToken(); + if (tokenFromProvider != null && !tokenFromProvider.isEmpty()) { + setAuthHeader(builder, tokenFromProvider); + } + } if (shouldProceed(response.request(), response)) { return CompletableFuture.completedFuture(false); } @@ -99,33 +105,18 @@ public CompletableFuture afterFailure(BasicBuilder builder, HttpRespons // use the original config, not the refreshed, as the username / password could be programmatically set on the Config or RequestConfig if (Utils.isNotNullOrEmpty(config.getUsername()) && Utils.isNotNullOrEmpty(config.getPassword())) { // TODO: we could make all concurrent refresh requests return the same future - return authorize().thenApply(t -> { - if (t != null) { - config.setOauthToken(t); - try { - // TODO: we may need some protection here or in the persistKubeConfigWithUpdatedAuthInfo - // if the user has modified the username via the requestconfig are we writing a valid value? - OpenIDConnectionUtils.persistKubeConfigWithUpdatedAuthInfo(config, a -> a.setToken(t)); - } catch (IOException e) { - LOGGER.warn("failure while persisting new token into KUBECONFIG", e); - } - // If token was obtained, then retry request using the obtained token. - return setAuthHeader(builder, t); - } - - return refreshFromConfig(builder); - }); + return authorize().thenApply(t -> persistNewOAuthTokenIntoKubeConfig(builder, t)); } return CompletableFuture.completedFuture(refreshFromConfig(builder)); } private boolean refreshFromConfig(BasicBuilder builder) { Config newestConfig = config.refresh(); // does some i/o work, but for now we'll consider this non-blocking - String oauthToken = newestConfig.getOauthToken(); + String oauthToken = newestConfig.getAutoOAuthToken(); if (oauthToken != null) { - config.setOauthToken(oauthToken); + config.setAutoOAuthToken(oauthToken); } - return setAuthHeader(builder, oauthToken); + return setAuthHeader(builder, config.getOauthToken()); } private boolean setAuthHeader(BasicBuilder builder, String token) { @@ -196,4 +187,21 @@ private boolean shouldProceed(HttpRequest request, HttpResponse response) { return response.code() != HTTP_UNAUTHORIZED; } + private boolean persistNewOAuthTokenIntoKubeConfig(BasicBuilder builder, String token) { + if (token != null) { + config.setAutoOAuthToken(token); + try { + // TODO: we may need some protection here or in the persistKubeConfigWithUpdatedAuthInfo + // if the user has modified the username via the requestconfig are we writing a valid value? + OpenIDConnectionUtils.persistKubeConfigWithUpdatedAuthInfo(config, a -> a.setToken(token)); + } catch (IOException e) { + LOGGER.warn("failure while persisting new token into KUBECONFIG", e); + } + // If token was obtained, then retry request using the obtained token. + return setAuthHeader(builder, token); + } + + return refreshFromConfig(builder); + } + } diff --git a/openshift-client/src/test/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptorTest.java b/openshift-client/src/test/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptorTest.java index c3c3a5c4bea..91cd3181d31 100644 --- a/openshift-client/src/test/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptorTest.java +++ b/openshift-client/src/test/java/io/fabric8/openshift/client/internal/OpenShiftOAuthInterceptorTest.java @@ -15,6 +15,9 @@ */ package io.fabric8.openshift.client.internal; +import io.fabric8.kubernetes.api.model.ContextBuilder; +import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder; +import io.fabric8.kubernetes.api.model.NamedContextBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.kubernetes.client.http.HttpRequest; @@ -22,20 +25,32 @@ import io.fabric8.kubernetes.client.http.StandardHttpRequest; import io.fabric8.kubernetes.client.http.TestHttpResponse; import io.fabric8.kubernetes.client.http.WebSocket; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; import io.fabric8.kubernetes.client.utils.TokenRefreshInterceptor; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.mockito.MockedStatic; +import java.io.File; import java.net.HttpURLConnection; import java.net.URI; import java.util.Collections; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.RETURNS_SELF; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class OpenShiftOAuthInterceptorTest { @@ -45,7 +60,7 @@ void testBasicAuthNotUsed() { config.setUsername("user"); config.setPassword("pass"); - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); @@ -62,9 +77,9 @@ void testTokenUsed() { Config config = Config.empty(); config.setUsername("user"); config.setPassword("pass"); - config.setOauthToken("token"); + config.setAutoOAuthToken("token"); - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); @@ -80,7 +95,7 @@ void testTokenUsed() { void testTokenRefreshedFromConfig() { Config config = mockConfigRefresh(); - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); @@ -91,33 +106,133 @@ void testTokenRefreshedFromConfig() { .withRequest(new StandardHttpRequest(null, URI.create("http://localhost"), "GET", null)), null); assertEquals(Collections.singletonList("Bearer token"), builder.build().headers(TokenRefreshInterceptor.AUTHORIZATION)); - Mockito.verify(config).setOauthToken("token"); + verify(config).setAutoOAuthToken("token"); + } + + @Test + void afterFailure_whenTokenSetByUser_thenNoRefresh() { + // Given + Config config = mock(Config.class, RETURNS_DEEP_STUBS); + when(config.getAutoOAuthToken()).thenReturn(null); + when(config.getOauthToken()).thenReturn("token-set-by-user"); + when(config.refresh()).thenReturn(config); + HttpClient client = mock(HttpClient.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, RETURNS_SELF); + OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); + + // When + CompletableFuture result = interceptor.afterFailure(builder, + TestHttpResponse.from(HttpURLConnection.HTTP_UNAUTHORIZED, "not for you") + .withRequest(new StandardHttpRequest(null, URI.create("http://localhost"), "GET", null)), + null); + + // Then + assertThat(result).isCompletedWithValue(true); + verify(builder).setHeader("Authorization", "Bearer token-set-by-user"); + } + + @Test + void afterFailure_whenOAuthTokenProviderPresent_thenUseTokenFromProvider() { + // Given + Config config = mock(Config.class, RETURNS_DEEP_STUBS); + when(config.getOauthTokenProvider()).thenReturn(() -> "token-from-oauthtokenprovider"); + HttpClient client = mock(HttpClient.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, RETURNS_SELF); + builder.uri("http://localhost"); + OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); + + // When + CompletableFuture result = interceptor.afterFailure(builder, + TestHttpResponse.from(HttpURLConnection.HTTP_UNAUTHORIZED, "not for you") + .withRequest(new StandardHttpRequest(null, URI.create("http://localhost"), "GET", null)), + null); + + // Then + assertThat(result).isCompletedWithValue(false); + verify(builder).setHeader("Authorization", "Bearer token-from-oauthtokenprovider"); + } + + @Test + void afterFailure_withUsernamePassword_thenShouldAuthorize() { + try (MockedStatic kubeConfigUtilsMockedStatic = mockStatic(KubeConfigUtils.class)) { + // Given + Config config = mock(Config.class, RETURNS_DEEP_STUBS); + io.fabric8.kubernetes.api.model.Config kubeConfigContent = mock(io.fabric8.kubernetes.api.model.Config.class); + HttpClient client = mock(HttpClient.class); + HttpRequest.Builder builder = mock(HttpRequest.Builder.class, RETURNS_SELF); + HttpRequest httpRequest = mock(HttpRequest.class); + HttpClient.DerivedClientBuilder derivedClientBuilder = mock(HttpClient.DerivedClientBuilder.class); + HttpResponse authEndpointHttpResponse = mock(HttpResponse.class); + HttpResponse authResponse = mock(HttpResponse.class); + CompletableFuture> authEndpointResponseCompletableFuture = new CompletableFuture<>(); + CompletableFuture> authResponseCompletableFuture = new CompletableFuture<>(); + authEndpointResponseCompletableFuture.complete(authEndpointHttpResponse); + authResponseCompletableFuture.complete(authResponse); + when(config.getUsername()).thenReturn("user"); + when(config.getPassword()).thenReturn("pass"); + when(config.getMasterUrl()).thenReturn("http://localhost:8443"); + when(config.getCurrentContext()).thenReturn(new NamedContextBuilder() + .withContext(new ContextBuilder() + .withUser("testuser") + .build()) + .build()); + when(config.getFile()).thenReturn(new File("kube/config")); + when(kubeConfigContent.getUsers()).thenReturn(Collections.singletonList(new NamedAuthInfoBuilder() + .withName("testuser") + .build())); + when(client.newBuilder()).thenReturn(derivedClientBuilder); + when(client.newHttpRequestBuilder()).thenReturn(builder); + when(builder.url(any())).thenReturn(builder); + when(builder.build()).thenReturn(httpRequest); + when(derivedClientBuilder.build()).thenReturn(client); + when(authEndpointHttpResponse.isSuccessful()).thenReturn(true); + when(authEndpointHttpResponse.code()).thenReturn(HTTP_OK); + when(authEndpointHttpResponse.body()).thenReturn("{\"authorization_endpoint\":\"https://oauth-test/oauth/authorize\"}"); + when(authResponse.previousResponse()).thenReturn(Optional.empty()); + when(authResponse.headers("Location")).thenReturn(Collections.singletonList( + "https://oauth-test/oauth/token/implicit#access_token=sha256~secret&expires_in=86400&scope=user%3Afull&token_type=Bearer")); + when(client.sendAsync(any(), any())) + .thenReturn(authEndpointResponseCompletableFuture) + .thenReturn(authResponseCompletableFuture); + kubeConfigUtilsMockedStatic.when(() -> KubeConfigUtils.parseConfig(any())).thenReturn(kubeConfigContent); + OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); + + // When + CompletableFuture result = interceptor.afterFailure(builder, + TestHttpResponse.from(HttpURLConnection.HTTP_UNAUTHORIZED, "not for you") + .withRequest(new StandardHttpRequest(null, URI.create("http://localhost"), "GET", null)), + null); + + // Then + assertThat(result).isCompletedWithValue(true); + kubeConfigUtilsMockedStatic.verify(() -> KubeConfigUtils.persistKubeConfigIntoFile(any(), anyString())); + } } @Test void testTokenRefreshedFromConfigForWebSocketBuilder() { Config config = mockConfigRefresh(); - Mockito.when(config.refresh().getOauthToken()).thenReturn("token"); + when(config.refresh().getAutoOAuthToken()).thenReturn("token"); - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); - WebSocket.Builder builder = Mockito.mock(WebSocket.Builder.class, Mockito.RETURNS_DEEP_STUBS); + WebSocket.Builder builder = mock(WebSocket.Builder.class, RETURNS_DEEP_STUBS); interceptor.afterFailure(builder, TestHttpResponse.from(HttpURLConnection.HTTP_UNAUTHORIZED, "not for you") .withRequest(new StandardHttpRequest(null, URI.create("http://localhost"), "GET", null)), null); - Mockito.verify(builder).setHeader(TokenRefreshInterceptor.AUTHORIZATION, "Bearer token"); - Mockito.verify(config).setOauthToken("token"); + verify(builder).setHeader(TokenRefreshInterceptor.AUTHORIZATION, "Bearer token"); + verify(config).setAutoOAuthToken("token"); } @Test void afterFailure_whenResponseCode403_thenShouldNotRefresh() { // Given - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); Config config = mockConfigRefresh(); HttpResponse httpResponse = mockHttpResponse(HTTP_FORBIDDEN); - HttpRequest.Builder httpRequestBuilder = Mockito.mock(HttpRequest.Builder.class); + HttpRequest.Builder httpRequestBuilder = mock(HttpRequest.Builder.class); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); // When @@ -130,9 +245,9 @@ void afterFailure_whenResponseCode403_thenShouldNotRefresh() { @Test void afterFailure_whenResponseCode401_thenShouldRefresh() { // Given - HttpClient client = Mockito.mock(HttpClient.class); + HttpClient client = mock(HttpClient.class); Config config = mockConfigRefresh(); - HttpRequest.Builder httpRequestBuilder = Mockito.mock(HttpRequest.Builder.class); + HttpRequest.Builder httpRequestBuilder = mock(HttpRequest.Builder.class); HttpResponse httpResponse = mockHttpResponse(HTTP_UNAUTHORIZED); OpenShiftOAuthInterceptor interceptor = new OpenShiftOAuthInterceptor(client, config); @@ -144,20 +259,22 @@ void afterFailure_whenResponseCode401_thenShouldRefresh() { } private Config mockConfigRefresh() { - Config config = Mockito.mock(Config.class); - Mockito.when(config.refresh()).thenReturn(config); - Mockito.when(config.getOauthToken()).thenReturn("token"); + Config config = mock(Config.class); + Config refreshedConfig = mock(Config.class); + when(refreshedConfig.getAutoOAuthToken()).thenReturn("token"); + when(config.refresh()).thenReturn(refreshedConfig); + when(config.getOauthToken()).thenReturn("token"); return config; } private HttpResponse mockHttpResponse(int responseCode) { - HttpRequest httpRequest = Mockito.mock(HttpRequest.class); - HttpResponse httpResponse = Mockito.mock(HttpResponse.class); - Mockito.when(httpRequest.method()).thenReturn("GET"); - Mockito.when(httpRequest.uri()) + HttpRequest httpRequest = mock(HttpRequest.class); + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpRequest.method()).thenReturn("GET"); + when(httpRequest.uri()) .thenReturn(URI.create("http://www.example.com/apis/routes.openshift.io/namespaces/foo/routes")); - Mockito.when(httpResponse.request()).thenReturn(httpRequest); - Mockito.when(httpResponse.code()).thenReturn(responseCode); + when(httpResponse.request()).thenReturn(httpRequest); + when(httpResponse.code()).thenReturn(responseCode); return httpResponse; }