From 143db15d03e8a6aa3a4240bce6cf9fba89936e0e Mon Sep 17 00:00:00 2001 From: arbielsk <39697028+arbielsk@users.noreply.github.com> Date: Tue, 19 Jan 2021 23:07:59 +0100 Subject: [PATCH] OIDC support (#544) * add minimal oidc support * add OidcTokenProvider * add null check for accessToken * deal with missing client-secret in config * fix formatting, typos * remove commented line * trigger github actions to check for non-deterministic test behavior * Update src/KubernetesClient/Authentication/OidcTokenProvider.cs Co-authored-by: Boshi Lian * Update src/KubernetesClient/Authentication/OidcTokenProvider.cs Co-authored-by: Boshi Lian * cleanup * add CA1723 to exceptions * remove exception for CA1723, add CA1724 instead Co-authored-by: Boshi Lian --- kubernetes-client.ruleset | 3 +- .../Authentication/OidcTokenProvider.cs | 65 +++++++++++++++++++ src/KubernetesClient/KubernetesClient.csproj | 1 + ...ubernetesClientConfiguration.ConfigFile.cs | 34 ++++++++-- 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/KubernetesClient/Authentication/OidcTokenProvider.cs diff --git a/kubernetes-client.ruleset b/kubernetes-client.ruleset index 8403c9b..f1221e9 100644 --- a/kubernetes-client.ruleset +++ b/kubernetes-client.ruleset @@ -300,6 +300,7 @@ - + + diff --git a/src/KubernetesClient/Authentication/OidcTokenProvider.cs b/src/KubernetesClient/Authentication/OidcTokenProvider.cs new file mode 100644 index 0000000..4301fd0 --- /dev/null +++ b/src/KubernetesClient/Authentication/OidcTokenProvider.cs @@ -0,0 +1,65 @@ +using System; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Rest; +using IdentityModel.OidcClient; +using k8s.Exceptions; + +namespace k8s.Authentication +{ + public class OidcTokenProvider : ITokenProvider + { + private OidcClient _oidcClient; + private string _idToken; + private string _refreshToken; + private string _accessToken; + private DateTime _expiry; + + public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerUrl, string idToken, string refreshToken) + { + _idToken = idToken; + _refreshToken = refreshToken; + _oidcClient = getClient(clientId, clientSecret, idpIssuerUrl); + } + + public async Task GetAuthenticationHeaderAsync(CancellationToken cancellationToken) + { + if (_expiry == null || _accessToken == null || DateTime.UtcNow.AddSeconds(30) > _expiry) + { + await RefreshToken().ConfigureAwait(false); + } + + return new AuthenticationHeaderValue("Bearer", _accessToken); + } + + private OidcClient getClient(string clientId, string clientSecret, string idpIssuerUrl) + { + OidcClientOptions options = new OidcClientOptions + { + ClientId = clientId, + ClientSecret = clientSecret ?? "", + Authority = idpIssuerUrl, + }; + + return new OidcClient(options); + } + + private async Task RefreshToken() + { + try + { + var result = + await _oidcClient.RefreshTokenAsync(_refreshToken).ConfigureAwait(false); + _accessToken = result.AccessToken; + _idToken = result.IdentityToken; + _refreshToken = result.RefreshToken; + _expiry = result.AccessTokenExpiration; + } + catch (Exception e) + { + throw new KubernetesClientException($"Unable to refresh OIDC token. \n {e.Message}", e); + } + } + } +} diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index 9bae5ce..3712b47 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -31,5 +31,6 @@ + diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index 0322989..06b983a 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -40,7 +40,7 @@ namespace k8s /// /// /// If multiple kubeconfig files are specified in the KUBECONFIG environment variable, - /// merges the files, where first occurence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. + /// merges the files, where first occurrence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. /// /// Instance of the class public static KubernetesClientConfiguration BuildDefaultConfig() @@ -214,7 +214,7 @@ namespace k8s } /// - /// Validates and Intializes Client Configuration + /// Validates and Initializes Client Configuration /// /// Kubernetes Configuration /// Current Context @@ -346,7 +346,8 @@ namespace k8s if (userDetails.UserCredentials.AuthProvider != null) { if (userDetails.UserCredentials.AuthProvider.Config != null - && userDetails.UserCredentials.AuthProvider.Config.ContainsKey("access-token")) + && (userDetails.UserCredentials.AuthProvider.Config.ContainsKey("access-token") + || userDetails.UserCredentials.AuthProvider.Config.ContainsKey("id-token"))) { switch (userDetails.UserCredentials.AuthProvider.Name) { @@ -390,6 +391,29 @@ namespace k8s userCredentialsFound = true; break; } + + case "oidc": + { + var config = userDetails.UserCredentials.AuthProvider.Config; + AccessToken = config["id-token"]; + if (config.ContainsKey("client-id") + && config.ContainsKey("idp-issuer-url") + && config.ContainsKey("id-token") + && config.ContainsKey("refresh-token")) + { + string clientId = config["client-id"]; + string clientSecret = config.ContainsKey("client-secret") ? config["client-secret"] : null; + string idpIssuerUrl = config["idp-issuer-url"]; + string idToken = config["id-token"]; + string refreshToken = config["refresh-token"]; + + TokenProvider = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, idToken, refreshToken); + + userCredentialsFound = true; + } + + break; + } } } } @@ -656,7 +680,7 @@ namespace k8s /// file is located. When , the paths will be considered to be relative to the current working directory. /// Instance of the class /// - /// The kube config files will be merges into a single , where first occurence wins. + /// The kube config files will be merges into a single , where first occurrence wins. /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. /// internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true) @@ -672,7 +696,7 @@ namespace k8s /// file is located. When , the paths will be considered to be relative to the current working directory. /// Instance of the class /// - /// The kube config files will be merges into a single , where first occurence wins. + /// The kube config files will be merges into a single , where first occurrence wins. /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. /// internal static async Task LoadKubeConfigAsync(