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 <farmer1992@gmail.com> * Update src/KubernetesClient/Authentication/OidcTokenProvider.cs Co-authored-by: Boshi Lian <farmer1992@gmail.com> * cleanup * add CA1723 to exceptions * remove exception for CA1723, add CA1724 instead Co-authored-by: Boshi Lian <farmer1992@gmail.com>
This commit is contained in:
@@ -300,6 +300,7 @@
|
||||
<Rule Id="CS1591" Action="None" />
|
||||
<Rule Id="CS1573" Action="None" />
|
||||
<Rule Id="CS1574" Action="None" />
|
||||
|
||||
<!-- Rename k8s.Extensions type to mitigate conflict with Microsoft.Extensions namespace https://github.com/kubernetes-client/csharp/pull/544#issuecomment-759230655-->
|
||||
<Rule Id="CA1724" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
|
||||
65
src/KubernetesClient/Authentication/OidcTokenProvider.cs
Normal file
65
src/KubernetesClient/Authentication/OidcTokenProvider.cs
Normal file
@@ -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<AuthenticationHeaderValue> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,5 +31,6 @@
|
||||
<PackageReference Include="YamlDotNet" Version="8.1.2" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="System.Buffers" Version="4.5.1" />
|
||||
<PackageReference Include="IdentityModel.OidcClient" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace k8s
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <returns>Instance of the<see cref="KubernetesClientConfiguration"/> class</returns>
|
||||
public static KubernetesClientConfiguration BuildDefaultConfig()
|
||||
@@ -214,7 +214,7 @@ namespace k8s
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and Intializes Client Configuration
|
||||
/// Validates and Initializes Client Configuration
|
||||
/// </summary>
|
||||
/// <param name="k8SConfig">Kubernetes Configuration</param>
|
||||
/// <param name="currentContext">Current Context</param>
|
||||
@@ -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 <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
|
||||
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
|
||||
/// <remarks>
|
||||
/// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurence wins.
|
||||
/// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurrence wins.
|
||||
/// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
|
||||
/// </remarks>
|
||||
internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true)
|
||||
@@ -672,7 +696,7 @@ namespace k8s
|
||||
/// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param>
|
||||
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
|
||||
/// <remarks>
|
||||
/// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurence wins.
|
||||
/// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurrence wins.
|
||||
/// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
|
||||
/// </remarks>
|
||||
internal static async Task<K8SConfiguration> LoadKubeConfigAsync(
|
||||
|
||||
Reference in New Issue
Block a user