fix: oidc (#633)
* fix: oidc * revert: verbose oidc logs * fix: actually commit changes * chore: cleanup var name * chore: address pr feedback
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using Microsoft.Rest;
|
using Microsoft.Rest;
|
||||||
using IdentityModel.OidcClient;
|
using IdentityModel.OidcClient;
|
||||||
using k8s.Exceptions;
|
using k8s.Exceptions;
|
||||||
@@ -13,7 +14,6 @@ namespace k8s.Authentication
|
|||||||
private OidcClient _oidcClient;
|
private OidcClient _oidcClient;
|
||||||
private string _idToken;
|
private string _idToken;
|
||||||
private string _refreshToken;
|
private string _refreshToken;
|
||||||
private string _accessToken;
|
|
||||||
private DateTime _expiry;
|
private DateTime _expiry;
|
||||||
|
|
||||||
public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerUrl, string idToken, string refreshToken)
|
public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerUrl, string idToken, string refreshToken)
|
||||||
@@ -21,16 +21,34 @@ namespace k8s.Authentication
|
|||||||
_idToken = idToken;
|
_idToken = idToken;
|
||||||
_refreshToken = refreshToken;
|
_refreshToken = refreshToken;
|
||||||
_oidcClient = getClient(clientId, clientSecret, idpIssuerUrl);
|
_oidcClient = getClient(clientId, clientSecret, idpIssuerUrl);
|
||||||
|
_expiry = getExpiryFromToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
|
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_accessToken == null || DateTime.UtcNow.AddSeconds(30) > _expiry)
|
if (_idToken == null || DateTime.UtcNow.AddSeconds(30) > _expiry)
|
||||||
{
|
{
|
||||||
await RefreshToken().ConfigureAwait(false);
|
await RefreshToken().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthenticationHeaderValue("Bearer", _accessToken);
|
return new AuthenticationHeaderValue("Bearer", _idToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime getExpiryFromToken()
|
||||||
|
{
|
||||||
|
int expiry;
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = handler.ReadJwtToken(_idToken);
|
||||||
|
expiry = token.Payload.Exp ?? 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
expiry = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(expiry).UtcDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OidcClient getClient(string clientId, string clientSecret, string idpIssuerUrl)
|
private OidcClient getClient(string clientId, string clientSecret, string idpIssuerUrl)
|
||||||
@@ -49,9 +67,13 @@ namespace k8s.Authentication
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result =
|
var result = await _oidcClient.RefreshTokenAsync(_refreshToken).ConfigureAwait(false);
|
||||||
await _oidcClient.RefreshTokenAsync(_refreshToken).ConfigureAwait(false);
|
|
||||||
_accessToken = result.AccessToken;
|
if (result.IsError)
|
||||||
|
{
|
||||||
|
throw new Exception(result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
_idToken = result.IdentityToken;
|
_idToken = result.IdentityToken;
|
||||||
_refreshToken = result.RefreshToken;
|
_refreshToken = result.RefreshToken;
|
||||||
_expiry = result.AccessTokenExpiration;
|
_expiry = result.AccessTokenExpiration;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.3" />
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.3" />
|
||||||
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.10" />
|
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.10" />
|
||||||
<PackageReference Include="prometheus-net" Version="4.1.1" />
|
<PackageReference Include="prometheus-net" Version="4.1.1" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.1" />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using System.Security.Cryptography;
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using k8s.Authentication;
|
||||||
|
using k8s.Exceptions;
|
||||||
using k8s.KubeConfigModels;
|
using k8s.KubeConfigModels;
|
||||||
using k8s.Models;
|
using k8s.Models;
|
||||||
using k8s.Tests.Mock;
|
using k8s.Tests.Mock;
|
||||||
@@ -440,6 +442,87 @@ namespace k8s.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Oidc()
|
||||||
|
{
|
||||||
|
var clientId = "CLIENT_ID";
|
||||||
|
var clientSecret = "CLIENT_SECRET";
|
||||||
|
var idpIssuerUrl = "https://idp.issuer.url";
|
||||||
|
var unexpiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjAsImV4cCI6MjAwMDAwMDAwMH0.8Ata5uKlrqYfeIaMwS91xVgVFHu7ntHx1sGN95i2Zho";
|
||||||
|
var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0";
|
||||||
|
var refreshToken = "REFRESH_TOKEN";
|
||||||
|
|
||||||
|
using (var server = new MockKubeApiServer(testOutput, cxt =>
|
||||||
|
{
|
||||||
|
var header = cxt.Request.Headers["Authorization"].FirstOrDefault();
|
||||||
|
|
||||||
|
var expect = new AuthenticationHeaderValue("Bearer", unexpiredIdToken).ToString();
|
||||||
|
|
||||||
|
if (header != expect)
|
||||||
|
{
|
||||||
|
cxt.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
{
|
||||||
|
// use unexpired id token as bearer, do not attempt to refresh
|
||||||
|
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||||
|
{
|
||||||
|
Host = server.Uri.ToString(),
|
||||||
|
AccessToken = unexpiredIdToken,
|
||||||
|
TokenProvider = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, unexpiredIdToken, refreshToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
var listTask = ExecuteListPods(client);
|
||||||
|
Assert.True(listTask.Response.IsSuccessStatusCode);
|
||||||
|
Assert.Equal(1, listTask.Body.Items.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// attempt to refresh id token when expired
|
||||||
|
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||||
|
{
|
||||||
|
Host = server.Uri.ToString(),
|
||||||
|
AccessToken = expiredIdToken,
|
||||||
|
TokenProvider = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PeelAggregate(() => ExecuteListPods(client));
|
||||||
|
Assert.True(false, "should not be here");
|
||||||
|
}
|
||||||
|
catch (KubernetesClientException e)
|
||||||
|
{
|
||||||
|
Assert.StartsWith("Unable to refresh OIDC token.", e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// attempt to refresh id token when null
|
||||||
|
var client = new Kubernetes(new KubernetesClientConfiguration
|
||||||
|
{
|
||||||
|
Host = server.Uri.ToString(),
|
||||||
|
AccessToken = expiredIdToken,
|
||||||
|
TokenProvider = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, null, refreshToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PeelAggregate(() => ExecuteListPods(client));
|
||||||
|
Assert.True(false, "should not be here");
|
||||||
|
}
|
||||||
|
catch (KubernetesClientException e)
|
||||||
|
{
|
||||||
|
Assert.StartsWith("Unable to refresh OIDC token.", e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ShouldThrowUnauthorized(Kubernetes client)
|
private static void ShouldThrowUnauthorized(Kubernetes client)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using k8s.Authentication;
|
||||||
using k8s.Exceptions;
|
using k8s.Exceptions;
|
||||||
using k8s.KubeConfigModels;
|
using k8s.KubeConfigModels;
|
||||||
using System;
|
using System;
|
||||||
@@ -245,6 +246,18 @@ namespace k8s.Tests
|
|||||||
Assert.Equal("secret", cfg.Password);
|
Assert.Equal("secret", cfg.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks oidc authentication provider information is read properly
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void UserOidcAuthentication()
|
||||||
|
{
|
||||||
|
var fi = new FileInfo("assets/kubeconfig.user-oidc.yml");
|
||||||
|
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, useRelativePaths: false);
|
||||||
|
Assert.Equal("ID_TOKEN", cfg.AccessToken);
|
||||||
|
Assert.IsType<OidcTokenProvider>(cfg.TokenProvider);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks that a KubeConfigException is thrown when user cannot be found in users
|
/// Checks that a KubeConfigException is thrown when user cannot be found in users
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
57
tests/KubernetesClient.Tests/OidcAuthTests.cs
Normal file
57
tests/KubernetesClient.Tests/OidcAuthTests.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using k8s.Authentication;
|
||||||
|
using k8s.Exceptions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace k8s.Tests
|
||||||
|
{
|
||||||
|
public class OidcAuthTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task TestOidcAuth()
|
||||||
|
{
|
||||||
|
var clientId = "CLIENT_ID";
|
||||||
|
var clientSecret = "CLIENT_SECRET";
|
||||||
|
var idpIssuerUrl = "https://idp.issuer.url";
|
||||||
|
var unexpiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjAsImV4cCI6MjAwMDAwMDAwMH0.8Ata5uKlrqYfeIaMwS91xVgVFHu7ntHx1sGN95i2Zho";
|
||||||
|
var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0";
|
||||||
|
var refreshToken = "REFRESH_TOKEN";
|
||||||
|
|
||||||
|
// use unexpired id token as bearer, do not attempt to refresh
|
||||||
|
var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, unexpiredIdToken, refreshToken);
|
||||||
|
var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
result.Scheme.Should().Be("Bearer");
|
||||||
|
result.Parameter.Should().Be(unexpiredIdToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// attempt to refresh id token when expired
|
||||||
|
auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken);
|
||||||
|
result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
result.Scheme.Should().Be("Bearer");
|
||||||
|
result.Parameter.Should().Be(expiredIdToken);
|
||||||
|
Assert.True(false, "should not be here");
|
||||||
|
}
|
||||||
|
catch (KubernetesClientException e)
|
||||||
|
{
|
||||||
|
Assert.StartsWith("Unable to refresh OIDC token.", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// attempt to refresh id token when null
|
||||||
|
auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, null, refreshToken);
|
||||||
|
result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
result.Scheme.Should().Be("Bearer");
|
||||||
|
result.Parameter.Should().Be(expiredIdToken);
|
||||||
|
Assert.True(false, "should not be here");
|
||||||
|
}
|
||||||
|
catch (KubernetesClientException e)
|
||||||
|
{
|
||||||
|
Assert.StartsWith("Unable to refresh OIDC token.", e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using k8s.Authentication;
|
using k8s.Authentication;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace k8s.Tests
|
namespace k8s.Tests
|
||||||
{
|
{
|
||||||
public class TokenFileAuthTests
|
public class TokenFileAuthTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
public async Task TestToken()
|
public async Task TestToken()
|
||||||
{
|
{
|
||||||
var auth = new TokenFileAuth("assets/token1");
|
var auth = new TokenFileAuth("assets/token1");
|
||||||
@@ -20,7 +22,7 @@ namespace k8s.Tests
|
|||||||
result.Scheme.Should().Be("Bearer");
|
result.Scheme.Should().Be("Bearer");
|
||||||
result.Parameter.Should().Be("token1");
|
result.Parameter.Should().Be("token1");
|
||||||
|
|
||||||
auth.TokenExpiresAt = DateTime.UtcNow;
|
auth.TokenExpiresAt = DateTime.UtcNow.AddSeconds(-1);
|
||||||
result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false);
|
result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
result.Scheme.Should().Be("Bearer");
|
result.Scheme.Should().Be("Bearer");
|
||||||
result.Parameter.Should().Be("token2");
|
result.Parameter.Should().Be("token2");
|
||||||
|
|||||||
28
tests/KubernetesClient.Tests/assets/kubeconfig.user-oidc.yml
Normal file
28
tests/KubernetesClient.Tests/assets/kubeconfig.user-oidc.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Sample file based on https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/
|
||||||
|
# WARNING: File includes minor fixes
|
||||||
|
---
|
||||||
|
current-context: federal-context
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: assets/ca.crt
|
||||||
|
server: https://horse.org:4443
|
||||||
|
name: horse-cluster
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: horse-cluster
|
||||||
|
namespace: chisel-ns
|
||||||
|
user: green-user
|
||||||
|
name: federal-context
|
||||||
|
kind: Config
|
||||||
|
users:
|
||||||
|
- name: green-user
|
||||||
|
user:
|
||||||
|
auth-provider:
|
||||||
|
config:
|
||||||
|
client-id: CLIENT_ID
|
||||||
|
client-secret: CLIENT_SECRET
|
||||||
|
id-token: ID_TOKEN
|
||||||
|
idp-issuer-url: IDP_ISSUER_URL
|
||||||
|
refresh-token: REFRESH_TOKEN
|
||||||
|
name: oidc
|
||||||
Reference in New Issue
Block a user