All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.fabric8.kubernetes.client.utils.OpenIDConnectionUtils Maven / Gradle / Ivy

/**
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * 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 io.fabric8.kubernetes.client.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
import io.fabric8.kubernetes.client.http.HttpResponse;
import io.fabric8.kubernetes.client.internal.KubeConfigUtils;
import io.fabric8.kubernetes.client.internal.SSLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Utility class for OpenID token refresh.
 */
public class OpenIDConnectionUtils {
  private static final Logger LOGGER = LoggerFactory.getLogger(OpenIDConnectionUtils.class);

  public static final String EMPTY = "";
  public static final String ID_TOKEN_KUBECONFIG = "id-token";
  public static final String ISSUER_KUBECONFIG = "idp-issuer-url";
  public static final String REFRESH_TOKEN_KUBECONFIG = "refresh-token";
  public static final String REFRESH_TOKEN_PARAM = "refresh_token";
  public static final String GRANT_TYPE_PARAM = "grant_type";
  public static final String CLIENT_ID_PARAM = "client_id";
  public static final String CLIENT_SECRET_PARAM = "client_secret";
  public static final String ID_TOKEN_PARAM = "id_token";
  public static final String ACCESS_TOKEN_PARAM = "access_token";
  public static final String CLIENT_ID_KUBECONFIG = "client-id";
  public static final String CLIENT_SECRET_KUBECONFIG = "client-secret";
  public static final String IDP_CERT_DATA = "idp-certificate-authority-data";
  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 OpenIDConnectionUtils() { }

  /**
   * Fetch OpenID Connect token from Kubeconfig, check whether it's still valid or not; If expired handle
   * token refresh with OpenID Connection provider APIs
   *
   * @param currentAuthProviderConfig current AuthInfo's AuthProvider config as a map
   * @return access token for interacting with Kubernetes API
   */
  public static String resolveOIDCTokenFromAuthConfig(Map currentAuthProviderConfig, HttpClient.Builder clientBuilder) {
    String accessToken = currentAuthProviderConfig.get(ID_TOKEN_KUBECONFIG);
    String issuer = currentAuthProviderConfig.get(ISSUER_KUBECONFIG);
    String clientId = currentAuthProviderConfig.get(CLIENT_ID_KUBECONFIG);
    String refreshToken = currentAuthProviderConfig.get(REFRESH_TOKEN_KUBECONFIG);
    String clientSecret = currentAuthProviderConfig.getOrDefault(CLIENT_SECRET_KUBECONFIG, "");
    String idpCert = currentAuthProviderConfig.get(IDP_CERT_DATA);
    if (isTokenRefreshSupported(currentAuthProviderConfig)) {
      return getOIDCProviderTokenEndpointAndRefreshToken(issuer, clientId, refreshToken, clientSecret, accessToken, idpCert, clientBuilder);
    }
    return 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 String getOIDCProviderTokenEndpointAndRefreshToken(HttpClient client, Map wellKnownOpenIdConfiguration, String clientId, String refreshToken, String clientSecret, String accessToken, boolean shouldPersistUpdatedTokenInKubeConfig) {
    String oidcTokenEndpoint = getParametersFromDiscoveryResponse(wellKnownOpenIdConfiguration, TOKEN_ENDPOINT_PARAM);
    try {
      String freshAccessToken = OpenIDConnectionUtils.refreshToken(client, oidcTokenEndpoint, clientId, refreshToken, clientSecret, shouldPersistUpdatedTokenInKubeConfig);
      if (freshAccessToken != null) {
        accessToken = freshAccessToken;
      }
    } catch (Exception e) {
      LOGGER.warn("Could not refresh OIDC token: {}", e.getMessage());
    }
    return accessToken;
  }

  /**
   * Whether we should try to do token refresh or not, checks whether refresh-token key is set in
   * HashMap or not
   *
   * @param currentAuthProviderConfig a HashMap of current AuthProvider configuration
   * @return boolean value whether refresh-token is present or not.
   */
  static boolean isTokenRefreshSupported(Map currentAuthProviderConfig) {
    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 String refreshToken(HttpClient client, String oidcTokenEndpoint, String clientId, String refreshToken, String clientSecret, boolean shouldPersistUpdatedTokenInKubeConfig) {
    try {
      Map response = refreshOidcToken(client, clientId, refreshToken, clientSecret, oidcTokenEndpoint);

      if (!response.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.
      if (shouldPersistUpdatedTokenInKubeConfig && !persistKubeConfigWithUpdatedToken(response)) {
        LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG");
      }
      return String.valueOf(response.get(ID_TOKEN_PARAM));
    } catch (IOException e) {
      LOGGER.warn("Failure in fetching refresh token: ", e);
    }
    return null;
  }


  /**
   * Issue Token Refresh HTTP Request to OIDC Provider
   *
   * @param client Http Client for issuing HTTP request
   * @param clientId client id
   * @param refreshToken refresh token
   * @param clientSecret client secret
   * @param tokenURL OpenID Connection provider's token refresh url
   * @return response as HashMap
   * @throws IOException in case of any error in contacting OIDC provider
   */
  static Map refreshOidcToken(HttpClient client, String clientId, String refreshToken, String clientSecret, String tokenURL) throws IOException {
    HttpRequest request = getTokenRefreshHttpRequest(client, tokenURL, clientId, refreshToken, clientSecret);
    HttpResponse response = client.send(request, String.class);
    String body = response.body();
    if (body != null) {
      // Get response body as string
      if (response.isSuccessful()) {
        // Deserialize response body into a Map and return
        return convertJsonStringToMap(body);
      } else {
        // Log error response body
        LOGGER.warn("Response: {}", body);
      }
    }
    return Collections.emptyMap();
  }

  /**
   * OpenID providers publish their metadata at a well-known URL OpenID providers publish their metadata
   * at a well-known URL which looks like this: https://[base-server-url]/.well-known/openid-configuration
   * This method performs an Http Get at this public URL and fetches response as a HashMap
   *
   * @param client HttpClient for doing HTTP Get to well known URL of OpenID provider
   * @param issuer OpenID Connect provider issuer URL
   * @return a HashMap of Discovery document
   */
  static Map getOIDCDiscoveryDocumentAsMap(HttpClient client, String issuer) {
    HttpRequest request = client.newHttpRequestBuilder().uri(getWellKnownUrlForOpenIDIssuer(issuer)).build();
    try {
      HttpResponse response = client.send(request, String.class);
      if (response.isSuccessful() && response.body() != null) {
        return convertJsonStringToMap(response.body());
      } else {
        // Don't produce an error that's too huge (e.g. if we get HTML back for some reason).
        String responseBody = response.body();
        LOGGER.warn("oidc: failed to query metadata endpoint: {} {}", response.code(), responseBody);
      }
    } catch (IOException e) {
      LOGGER.warn("Could not refresh OIDC token, failure in getting refresh URL", e);
    }
    return Collections.emptyMap();
  }

  /**
   * Well known URL for getting OpenID Connect metadata.
   * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
   *
   * @param issuer issuing authority URL
   * @return well known URL for corresponding OpenID provider
   */
  static String getWellKnownUrlForOpenIDIssuer(String issuer) {
    return URLUtils.join(issuer, "/", WELL_KNOWN_OPENID_CONFIGURATION);
  }

  /**
   * Parses a specified parameter from OpenID Connect Discovery document
   *
   * @param responseAsJson discovery document as HashMap
   * @param key parameter whose value needs to be fetched
   * @return value of parameter passed.
   */
  static String getParametersFromDiscoveryResponse(Map responseAsJson, String key) {
    // Metadata object. We only care about the token_endpoint, the thing endpoint
    // we'll be refreshing against.
    //
    // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
    if (responseAsJson.containsKey(key)) {
      return String.valueOf(responseAsJson.get(key));
    } else {
      LOGGER.warn("oidc: oidc: discovery object doesn't contain a {}", key);
    }
    return EMPTY;
  }

  /**
   * 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
   * @throws IOException in case of any failure while writing file
   */
  static boolean persistKubeConfigWithUpdatedToken(String kubeConfigPath, Map 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 authProviderConfig = config.getUsers().get(currentUserIndex).getUser().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 false;
  }

  private static Map convertJsonStringToMap(String jsonString) throws JsonProcessingException {
    return Serialization.jsonMapper().readValue(jsonString, Map.class);
  }

  private static HttpClient getDefaultHttpClientWithPemCert(String idpCert, HttpClient.Builder clientBuilder) {
    SSLContext sslContext = null;
    TrustManager[] trustManagers = null;
    // fist, lets get the pem
    String pemCert = new String(java.util.Base64.getDecoder().decode(idpCert));

    try {
      trustManagers = SSLUtils.trustManagers(pemCert, null, false, null, null);
      KeyManager[] keyManagers = SSLUtils.keyManagers(pemCert, null, null, null, null, null, null, null);
      sslContext = SSLUtils.sslContext(keyManagers, trustManagers);
    } catch (KeyStoreException |
      KeyManagementException |
      InvalidKeySpecException |
      NoSuchAlgorithmException |
      IOException |
      UnrecoverableKeyException |
      CertificateException e) {
      throw new RuntimeException("Could not import idp certificate", e);
    }
    
    if (sslContext != null) {
      clientBuilder.sslContext(sslContext, trustManagers);
    }
    return clientBuilder.build();
  }

  private static HttpRequest getTokenRefreshHttpRequest(HttpClient client, String tokenEndpointUrl, String clientId, String refreshToken, String clientSecret) {
    HttpRequest.Builder httpRequestBuilder = client.newHttpRequestBuilder().uri(tokenEndpointUrl);

    Map requestBody = getRequestBodyContentForRefresh(clientId, refreshToken, clientSecret);
    String credentials =
      java.util.Base64.getEncoder()
        .encodeToString((clientId + ':' + clientSecret).getBytes(StandardCharsets.UTF_8));
    httpRequestBuilder.header("Authorization", "Basic " + credentials);
    httpRequestBuilder.post(requestBody);
    return httpRequestBuilder.build();
  }

  private static Map getRequestBodyContentForRefresh(String clientId, String refreshToken, String clientSecret) {
    Map result = new LinkedHashMap<>();
    result.put(REFRESH_TOKEN_PARAM, refreshToken);
    result.put(GRANT_TYPE_PARAM, GRANT_TYPE_REFRESH_TOKEN);
    result.put(CLIENT_ID_PARAM, clientId);
    result.put(CLIENT_SECRET_PARAM, clientSecret);
    return result;
  }

  private static String getOIDCProviderTokenEndpointAndRefreshToken(String issuer, String clientId, String refreshToken, String clientSecret, String accessToken, String idpCert, HttpClient.Builder clientBuilder) {
    try (HttpClient newClient = getDefaultHttpClientWithPemCert(idpCert, clientBuilder)) {
      Map wellKnownOpenIdConfiguration = getOIDCDiscoveryDocumentAsMap(newClient, issuer);
      return getOIDCProviderTokenEndpointAndRefreshToken(newClient, wellKnownOpenIdConfiguration, clientId, refreshToken, clientSecret, accessToken, true);
    }
  }

  private static boolean persistKubeConfigWithUpdatedToken(Map updatedAuthProviderConfig) throws IOException {
    return persistKubeConfigWithUpdatedToken(io.fabric8.kubernetes.client.Config.getKubeconfigFilename(), updatedAuthProviderConfig);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy