Skip to content

Commit 27d706d

Browse files
brendandburnstg123
andauthored
Add tests to #1618 (#1621)
* Refactor OidcTokenProvider to remove dependency on IdentityModel and improve token handling * Improve OidcTokenProvider error handling and expiry setting The constructor `OidcTokenProvider` now always sets the `_expiry` field by calling `GetExpiryFromToken()`, regardless of whether `_idToken` is null or empty, removing the previous check for a non-empty `_idToken`. The `GetExpiryFromToken` method has been updated to handle invalid JWT token formats more gracefully. Instead of throwing an `ArgumentException` when the token format is invalid or when the 'exp' claim is missing, the method now returns a default value. The logic for parsing the JWT token and extracting the 'exp' claim has been wrapped in a try-catch block. If any exception occurs during this process, it is caught, and the method returns a default value instead of throwing an exception. * Refactor parts initialization inside try block Moved the initialization of the `parts` variable, which splits the `_idToken` string, inside the `try` block. Removed the previous check for exactly three elements in the `parts` array and the default return value if the check failed. * Add tests. --------- Co-authored-by: Boshi Lian <[email protected]>
1 parent 675de3d commit 27d706d

File tree

5 files changed

+200
-80
lines changed

5 files changed

+200
-80
lines changed

Directory.Packages.props

+54-53
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,55 @@
1-
<Project>
2-
<PropertyGroup>
3-
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4-
</PropertyGroup>
5-
<ItemGroup>
6-
<PackageVersion Include="AutoMapper" Version="13.0.1" />
7-
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.5.0" />
8-
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
9-
<PackageVersion Include="Fractions" Version="7.3.0" />
10-
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
11-
<PackageVersion Include="JsonPatch.Net" Version="2.1.0" />
12-
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
13-
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
14-
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
15-
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
16-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
17-
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="17.12.0" />
18-
<PackageVersion Include="Moq" Version="4.20.72" />
19-
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
20-
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
21-
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.7.0" />
22-
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
23-
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
24-
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
25-
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
26-
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.0" />
27-
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
28-
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="21.2.1" />
29-
<PackageVersion Include="System.Reactive" Version="6.0.1" />
30-
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
31-
<PackageVersion Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" Version="16.3.0" />
32-
<PackageVersion Include="xunit" Version="2.9.2" />
33-
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
34-
<PackageVersion Include="Xunit.StaFact" Version="1.1.11" />
35-
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
36-
</ItemGroup>
37-
<ItemGroup>
38-
<PackageVersion Include="Autofac" Version="8.2.0" />
39-
<PackageVersion Include="CaseExtensions" Version="1.1.0" />
40-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
41-
<PackageVersion Include="Namotion.Reflection" Version="3.0.1" />
42-
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
43-
<PackageVersion Include="NJsonSchema" Version="10.9.0" />
44-
<PackageVersion Include="NSwag.Core" Version="13.20.0" />
45-
<PackageVersion Include="Scriban" Version="5.9.1" />
46-
</ItemGroup>
47-
<ItemGroup>
48-
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
49-
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
50-
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="12.0.3" />
51-
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.112" />
52-
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
53-
</ItemGroup>
1+
<Project>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
<PackageVersion Include="AutoMapper" Version="13.0.1" />
7+
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.5.0" />
8+
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
9+
<PackageVersion Include="Fractions" Version="7.3.0" />
10+
<PackageVersion Include="IdentityModel.OidcClient" Version="6.0.0" />
11+
<PackageVersion Include="JsonPatch.Net" Version="2.1.0" />
12+
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
13+
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
14+
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
15+
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
16+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
17+
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="17.12.0" />
18+
<PackageVersion Include="Moq" Version="4.20.72" />
19+
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
20+
<PackageVersion Include="Nito.AsyncEx.Coordination" Version="5.1.2" />
21+
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.7.0" />
22+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
23+
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
24+
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
25+
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
26+
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.0" />
27+
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
28+
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="21.2.1" />
29+
<PackageVersion Include="System.Reactive" Version="6.0.1" />
30+
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
31+
<PackageVersion Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" Version="16.3.0" />
32+
<PackageVersion Include="Wiremock.Net" Version="1.7.4" />
33+
<PackageVersion Include="xunit" Version="2.9.2" />
34+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
35+
<PackageVersion Include="Xunit.StaFact" Version="1.1.11" />
36+
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
37+
</ItemGroup>
38+
<ItemGroup>
39+
<PackageVersion Include="Autofac" Version="8.2.0" />
40+
<PackageVersion Include="CaseExtensions" Version="1.1.0" />
41+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
42+
<PackageVersion Include="Namotion.Reflection" Version="3.0.1" />
43+
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
44+
<PackageVersion Include="NJsonSchema" Version="10.9.0" />
45+
<PackageVersion Include="NSwag.Core" Version="13.20.0" />
46+
<PackageVersion Include="Scriban" Version="5.9.1" />
47+
</ItemGroup>
48+
<ItemGroup>
49+
<GlobalPackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
50+
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
51+
<GlobalPackageReference Include="Microsoft.VisualStudio.SlnGen" Version="12.0.3" />
52+
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.112" />
53+
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
54+
</ItemGroup>
5455
</Project>

src/KubernetesClient/Authentication/OidcTokenProvider.cs

+58-25
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
using IdentityModel.OidcClient;
21
using k8s.Exceptions;
3-
using System.IdentityModel.Tokens.Jwt;
2+
using System.Net.Http;
43
using System.Net.Http.Headers;
4+
using System.Text;
55

66
namespace k8s.Authentication
77
{
88
public class OidcTokenProvider : ITokenProvider
99
{
10-
private readonly OidcClient _oidcClient;
10+
private readonly string _clientId;
11+
private readonly string _clientSecret;
12+
private readonly string _idpIssuerUrl;
13+
1114
private string _idToken;
1215
private string _refreshToken;
1316
private DateTimeOffset _expiry;
1417

1518
public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerUrl, string idToken, string refreshToken)
1619
{
20+
_clientId = clientId;
21+
_clientSecret = clientSecret;
22+
_idpIssuerUrl = idpIssuerUrl;
1723
_idToken = idToken;
1824
_refreshToken = refreshToken;
19-
_oidcClient = getClient(clientId, clientSecret, idpIssuerUrl);
20-
_expiry = getExpiryFromToken();
25+
_expiry = GetExpiryFromToken();
2126
}
2227

2328
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
@@ -30,49 +35,77 @@ public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(Cancel
3035
return new AuthenticationHeaderValue("Bearer", _idToken);
3136
}
3237

33-
private DateTime getExpiryFromToken()
38+
private DateTimeOffset GetExpiryFromToken()
3439
{
35-
long expiry;
36-
var handler = new JwtSecurityTokenHandler();
3740
try
3841
{
39-
var token = handler.ReadJwtToken(_idToken);
40-
expiry = token.Payload.Expiration ?? 0;
42+
var parts = _idToken.Split('.');
43+
var payload = parts[1];
44+
var jsonBytes = Base64UrlDecode(payload);
45+
var json = Encoding.UTF8.GetString(jsonBytes);
46+
47+
using var document = JsonDocument.Parse(json);
48+
if (document.RootElement.TryGetProperty("exp", out var expElement))
49+
{
50+
var exp = expElement.GetInt64();
51+
return DateTimeOffset.FromUnixTimeSeconds(exp);
52+
}
4153
}
4254
catch
4355
{
44-
expiry = 0;
56+
// ignore to default
4557
}
4658

47-
return DateTimeOffset.FromUnixTimeSeconds(expiry).UtcDateTime;
59+
return default;
4860
}
4961

50-
private OidcClient getClient(string clientId, string clientSecret, string idpIssuerUrl)
62+
private static byte[] Base64UrlDecode(string input)
5163
{
52-
OidcClientOptions options = new OidcClientOptions
64+
var output = input.Replace('-', '+').Replace('_', '/');
65+
switch (output.Length % 4)
5366
{
54-
ClientId = clientId,
55-
ClientSecret = clientSecret ?? "",
56-
Authority = idpIssuerUrl,
57-
};
67+
case 2: output += "=="; break;
68+
case 3: output += "="; break;
69+
}
5870

59-
return new OidcClient(options);
71+
return Convert.FromBase64String(output);
6072
}
6173

6274
private async Task RefreshToken()
6375
{
6476
try
6577
{
66-
var result = await _oidcClient.RefreshTokenAsync(_refreshToken).ConfigureAwait(false);
78+
using var httpClient = new HttpClient();
79+
var request = new HttpRequestMessage(HttpMethod.Post, _idpIssuerUrl);
80+
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
81+
{
82+
{ "grant_type", "refresh_token" },
83+
{ "client_id", _clientId },
84+
{ "client_secret", _clientSecret },
85+
{ "refresh_token", _refreshToken },
86+
});
87+
88+
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
89+
response.EnsureSuccessStatusCode();
6790

68-
if (result.IsError)
91+
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
92+
var jsonDocument = JsonDocument.Parse(responseContent);
93+
94+
if (jsonDocument.RootElement.TryGetProperty("id_token", out var idTokenElement))
95+
{
96+
_idToken = idTokenElement.GetString();
97+
}
98+
99+
if (jsonDocument.RootElement.TryGetProperty("refresh_token", out var refreshTokenElement))
69100
{
70-
throw new Exception(result.Error);
101+
_refreshToken = refreshTokenElement.GetString();
71102
}
72103

73-
_idToken = result.IdentityToken;
74-
_refreshToken = result.RefreshToken;
75-
_expiry = result.AccessTokenExpiration;
104+
if (jsonDocument.RootElement.TryGetProperty("expires_in", out var expiresInElement))
105+
{
106+
var expiresIn = expiresInElement.GetInt32();
107+
_expiry = DateTimeOffset.UtcNow.AddSeconds(expiresIn);
108+
}
76109
}
77110
catch (Exception e)
78111
{

src/KubernetesClient/KubernetesClient.csproj

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
11-
<PackageReference Include="IdentityModel.OidcClient" />
1210
<PackageReference Include="Fractions" />
1311
<PackageReference Include="YamlDotNet" />
1412
</ItemGroup>

tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageReference Include="System.Reactive" />
1414
<PackageReference Include="Nito.AsyncEx" />
1515
<PackageReference Include="Portable.BouncyCastle" />
16+
<PackageReference Include="Wiremock.Net" />
1617
</ItemGroup>
1718

1819
<ItemGroup>

tests/KubernetesClient.Tests/OidcAuthTests.cs

+87
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
using FluentAssertions;
22
using k8s.Authentication;
33
using k8s.Exceptions;
4+
using System;
5+
using System.Net;
46
using System.Threading;
57
using System.Threading.Tasks;
8+
using WireMock.Server;
9+
using WireMock.RequestBuilders;
10+
using WireMock.ResponseBuilders;
611
using Xunit;
712

813
namespace k8s.Tests
@@ -53,5 +58,87 @@ public async Task TestOidcAuth()
5358
Assert.StartsWith("Unable to refresh OIDC token.", e.Message);
5459
}
5560
}
61+
62+
[Fact]
63+
public async Task TestOidcAuthWithWireMock()
64+
{
65+
// Arrange
66+
var server = WireMockServer.Start();
67+
var idpIssuerUrl = server.Url + "/token";
68+
var clientId = "CLIENT_ID";
69+
var clientSecret = "CLIENT_SECRET";
70+
var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0";
71+
var refreshToken = "REFRESH_TOKEN";
72+
var newIdToken = "NEW_ID_TOKEN";
73+
var expiresIn = 3600;
74+
75+
// Simulate a successful token refresh response
76+
server
77+
.Given(Request.Create().WithPath("/token").UsingPost())
78+
.RespondWith(Response.Create()
79+
.WithStatusCode(HttpStatusCode.OK)
80+
.WithBody($@"{{
81+
""id_token"": ""{newIdToken}"",
82+
""refresh_token"": ""{refreshToken}"",
83+
""expires_in"": {expiresIn}
84+
}}"));
85+
86+
var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken);
87+
88+
// Act
89+
var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None);
90+
91+
// Assert
92+
result.Scheme.Should().Be("Bearer");
93+
result.Parameter.Should().Be(newIdToken);
94+
95+
// Verify that the expiry is set correctly
96+
var expectedExpiry = DateTimeOffset.UtcNow.AddSeconds(expiresIn);
97+
var actualExpiry = typeof(OidcTokenProvider)
98+
.GetField("_expiry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
99+
?.GetValue(auth) as DateTimeOffset?;
100+
actualExpiry.Should().NotBeNull();
101+
actualExpiry.Value.Should().BeCloseTo(expectedExpiry, precision: TimeSpan.FromSeconds(5));
102+
103+
// Verify that the refresh token is set correctly
104+
var actualRefreshToken = typeof(OidcTokenProvider)
105+
.GetField("_refreshToken", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
106+
?.GetValue(auth) as string;
107+
actualRefreshToken.Should().NotBeNull();
108+
actualRefreshToken.Should().Be(refreshToken);
109+
110+
// Stop the server
111+
server.Stop();
112+
}
113+
114+
[Fact]
115+
public async Task TestOidcAuthWithServerError()
116+
{
117+
// Arrange
118+
var server = WireMockServer.Start();
119+
var idpIssuerUrl = server.Url + "/token";
120+
var clientId = "CLIENT_ID";
121+
var clientSecret = "CLIENT_SECRET";
122+
var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0";
123+
var refreshToken = "REFRESH_TOKEN";
124+
125+
// Simulate a server error response
126+
server
127+
.Given(Request.Create().WithPath("/token").UsingPost())
128+
.RespondWith(Response.Create()
129+
.WithStatusCode(HttpStatusCode.InternalServerError)
130+
.WithBody(@"{ ""error"": ""server_error"" }"));
131+
132+
var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken);
133+
134+
// Act & Assert
135+
var exception = await Assert.ThrowsAsync<KubernetesClientException>(
136+
() => auth.GetAuthenticationHeaderAsync(CancellationToken.None));
137+
exception.Message.Should().StartWith("Unable to refresh OIDC token.");
138+
exception.InnerException.Message.Should().Contain("500");
139+
140+
// Stop the server
141+
server.Stop();
142+
}
56143
}
57144
}

0 commit comments

Comments
 (0)