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:
arbielsk
2021-01-19 23:07:59 +01:00
committed by GitHub
parent 97ed40c5a8
commit 143db15d03
4 changed files with 97 additions and 6 deletions

View File

@@ -300,6 +300,7 @@
<Rule Id="CS1591" Action="None" /> <Rule Id="CS1591" Action="None" />
<Rule Id="CS1573" Action="None" /> <Rule Id="CS1573" Action="None" />
<Rule Id="CS1574" 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> </Rules>
</RuleSet> </RuleSet>

View 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);
}
}
}
}

View File

@@ -31,5 +31,6 @@
<PackageReference Include="YamlDotNet" Version="8.1.2" /> <PackageReference Include="YamlDotNet" Version="8.1.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.Buffers" Version="4.5.1" /> <PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="IdentityModel.OidcClient" Version="3.1.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -40,7 +40,7 @@ namespace k8s
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If multiple kubeconfig files are specified in the KUBECONFIG environment variable, /// 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> /// </remarks>
/// <returns>Instance of the<see cref="KubernetesClientConfiguration"/> class</returns> /// <returns>Instance of the<see cref="KubernetesClientConfiguration"/> class</returns>
public static KubernetesClientConfiguration BuildDefaultConfig() public static KubernetesClientConfiguration BuildDefaultConfig()
@@ -214,7 +214,7 @@ namespace k8s
} }
/// <summary> /// <summary>
/// Validates and Intializes Client Configuration /// Validates and Initializes Client Configuration
/// </summary> /// </summary>
/// <param name="k8SConfig">Kubernetes Configuration</param> /// <param name="k8SConfig">Kubernetes Configuration</param>
/// <param name="currentContext">Current Context</param> /// <param name="currentContext">Current Context</param>
@@ -346,7 +346,8 @@ namespace k8s
if (userDetails.UserCredentials.AuthProvider != null) if (userDetails.UserCredentials.AuthProvider != null)
{ {
if (userDetails.UserCredentials.AuthProvider.Config != 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) switch (userDetails.UserCredentials.AuthProvider.Name)
{ {
@@ -390,6 +391,29 @@ namespace k8s
userCredentialsFound = true; userCredentialsFound = true;
break; 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> /// 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> /// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
/// <remarks> /// <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. /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
/// </remarks> /// </remarks>
internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true) 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> /// 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> /// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
/// <remarks> /// <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. /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
/// </remarks> /// </remarks>
internal static async Task<K8SConfiguration> LoadKubeConfigAsync( internal static async Task<K8SConfiguration> LoadKubeConfigAsync(