From 4b19366e72177eded7587ef1acebe06f1184bbd5 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Tue, 11 Feb 2025 11:29:59 -0800 Subject: [PATCH 1/4] Update upload-artifact action to version 4 in buildtest workflow (#1615) * Update upload-artifact action to version 4 in buildtest workflow * Disable parallel build for E2E tests in buildtest workflow * Update artifact name to include OS in buildtest workflow --- .github/workflows/buildtest.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml index 05224a51e..a31e0e50f 100644 --- a/.github/workflows/buildtest.yaml +++ b/.github/workflows/buildtest.yaml @@ -31,9 +31,9 @@ jobs: directory: ./TestResults files: '*.cobertura.xml' - name: Upload test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-${{ matrix.os }} path: ./TestResults if: ${{ always() }} # Always run this step even on failure @@ -73,7 +73,7 @@ jobs: - name: Test run: | true > skip.log - env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Tests --logger "SkipTestLogger;file=$PWD/skip.log" + env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false if [ -s skip.log ]; then cat skip.log echo "CASES MUST NOT BE SKIPPED" @@ -82,7 +82,7 @@ jobs: - name: AOT Test run: | true > skip.log - env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Aot.Tests --logger "SkipTestLogger;file=$PWD/skip.log" + env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Aot.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false if [ -s skip.log ]; then cat skip.log echo "CASES MUST NOT BE SKIPPED" From 675de3da2ada6dbc9d7870a29829036781b73c91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:03:58 -0800 Subject: [PATCH 2/4] Bump nunit/docfx-action from 3.4.2 to 4.0.0 (#1614) Bumps [nunit/docfx-action](https://github.com/nunit/docfx-action) from 3.4.2 to 4.0.0. - [Release notes](https://github.com/nunit/docfx-action/releases) - [Commits](https://github.com/nunit/docfx-action/compare/v3.4.2...v4.0.0) --- updated-dependencies: - dependency-name: nunit/docfx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docfx.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docfx.yaml b/.github/workflows/docfx.yaml index 9f565f324..b437c1b5f 100644 --- a/.github/workflows/docfx.yaml +++ b/.github/workflows/docfx.yaml @@ -39,7 +39,7 @@ jobs: - name: Build run: dotnet build -c Release - - uses: nunit/docfx-action@v3.4.2 + - uses: nunit/docfx-action@v4.0.0 name: Build Documentation with: args: doc/docfx.json From 27d706d21e609c768a1c105c4e9f86863da1cc57 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:13:07 -0700 Subject: [PATCH 3/4] 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 --- Directory.Packages.props | 107 +++++++++--------- .../Authentication/OidcTokenProvider.cs | 83 ++++++++++---- src/KubernetesClient/KubernetesClient.csproj | 2 - .../KubernetesClient.Tests.csproj | 1 + tests/KubernetesClient.Tests/OidcAuthTests.cs | 87 ++++++++++++++ 5 files changed, 200 insertions(+), 80 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f1bd2cccc..c224b2acb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,54 +1,55 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/KubernetesClient/Authentication/OidcTokenProvider.cs b/src/KubernetesClient/Authentication/OidcTokenProvider.cs index ef9c35403..912ea0fde 100644 --- a/src/KubernetesClient/Authentication/OidcTokenProvider.cs +++ b/src/KubernetesClient/Authentication/OidcTokenProvider.cs @@ -1,23 +1,28 @@ -using IdentityModel.OidcClient; using k8s.Exceptions; -using System.IdentityModel.Tokens.Jwt; +using System.Net.Http; using System.Net.Http.Headers; +using System.Text; namespace k8s.Authentication { public class OidcTokenProvider : ITokenProvider { - private readonly OidcClient _oidcClient; + private readonly string _clientId; + private readonly string _clientSecret; + private readonly string _idpIssuerUrl; + private string _idToken; private string _refreshToken; private DateTimeOffset _expiry; public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerUrl, string idToken, string refreshToken) { + _clientId = clientId; + _clientSecret = clientSecret; + _idpIssuerUrl = idpIssuerUrl; _idToken = idToken; _refreshToken = refreshToken; - _oidcClient = getClient(clientId, clientSecret, idpIssuerUrl); - _expiry = getExpiryFromToken(); + _expiry = GetExpiryFromToken(); } public async Task GetAuthenticationHeaderAsync(CancellationToken cancellationToken) @@ -30,49 +35,77 @@ public async Task GetAuthenticationHeaderAsync(Cancel return new AuthenticationHeaderValue("Bearer", _idToken); } - private DateTime getExpiryFromToken() + private DateTimeOffset GetExpiryFromToken() { - long expiry; - var handler = new JwtSecurityTokenHandler(); try { - var token = handler.ReadJwtToken(_idToken); - expiry = token.Payload.Expiration ?? 0; + var parts = _idToken.Split('.'); + var payload = parts[1]; + var jsonBytes = Base64UrlDecode(payload); + var json = Encoding.UTF8.GetString(jsonBytes); + + using var document = JsonDocument.Parse(json); + if (document.RootElement.TryGetProperty("exp", out var expElement)) + { + var exp = expElement.GetInt64(); + return DateTimeOffset.FromUnixTimeSeconds(exp); + } } catch { - expiry = 0; + // ignore to default } - return DateTimeOffset.FromUnixTimeSeconds(expiry).UtcDateTime; + return default; } - private OidcClient getClient(string clientId, string clientSecret, string idpIssuerUrl) + private static byte[] Base64UrlDecode(string input) { - OidcClientOptions options = new OidcClientOptions + var output = input.Replace('-', '+').Replace('_', '/'); + switch (output.Length % 4) { - ClientId = clientId, - ClientSecret = clientSecret ?? "", - Authority = idpIssuerUrl, - }; + case 2: output += "=="; break; + case 3: output += "="; break; + } - return new OidcClient(options); + return Convert.FromBase64String(output); } private async Task RefreshToken() { try { - var result = await _oidcClient.RefreshTokenAsync(_refreshToken).ConfigureAwait(false); + using var httpClient = new HttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, _idpIssuerUrl); + request.Content = new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "refresh_token" }, + { "client_id", _clientId }, + { "client_secret", _clientSecret }, + { "refresh_token", _refreshToken }, + }); + + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - if (result.IsError) + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var jsonDocument = JsonDocument.Parse(responseContent); + + if (jsonDocument.RootElement.TryGetProperty("id_token", out var idTokenElement)) + { + _idToken = idTokenElement.GetString(); + } + + if (jsonDocument.RootElement.TryGetProperty("refresh_token", out var refreshTokenElement)) { - throw new Exception(result.Error); + _refreshToken = refreshTokenElement.GetString(); } - _idToken = result.IdentityToken; - _refreshToken = result.RefreshToken; - _expiry = result.AccessTokenExpiration; + if (jsonDocument.RootElement.TryGetProperty("expires_in", out var expiresInElement)) + { + var expiresIn = expiresInElement.GetInt32(); + _expiry = DateTimeOffset.UtcNow.AddSeconds(expiresIn); + } } catch (Exception e) { diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index fc8ea8ac5..dba319136 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -7,8 +7,6 @@ - - diff --git a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj index a87beb7b6..fade8e304 100644 --- a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj +++ b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/KubernetesClient.Tests/OidcAuthTests.cs b/tests/KubernetesClient.Tests/OidcAuthTests.cs index 23eb7292d..c05a52f10 100644 --- a/tests/KubernetesClient.Tests/OidcAuthTests.cs +++ b/tests/KubernetesClient.Tests/OidcAuthTests.cs @@ -1,8 +1,13 @@ using FluentAssertions; using k8s.Authentication; using k8s.Exceptions; +using System; +using System.Net; using System.Threading; using System.Threading.Tasks; +using WireMock.Server; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; using Xunit; namespace k8s.Tests @@ -53,5 +58,87 @@ public async Task TestOidcAuth() Assert.StartsWith("Unable to refresh OIDC token.", e.Message); } } + + [Fact] + public async Task TestOidcAuthWithWireMock() + { + // Arrange + var server = WireMockServer.Start(); + var idpIssuerUrl = server.Url + "/token"; + var clientId = "CLIENT_ID"; + var clientSecret = "CLIENT_SECRET"; + var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0"; + var refreshToken = "REFRESH_TOKEN"; + var newIdToken = "NEW_ID_TOKEN"; + var expiresIn = 3600; + + // Simulate a successful token refresh response + server + .Given(Request.Create().WithPath("/token").UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithBody($@"{{ + ""id_token"": ""{newIdToken}"", + ""refresh_token"": ""{refreshToken}"", + ""expires_in"": {expiresIn} + }}")); + + var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken); + + // Act + var result = await auth.GetAuthenticationHeaderAsync(CancellationToken.None); + + // Assert + result.Scheme.Should().Be("Bearer"); + result.Parameter.Should().Be(newIdToken); + + // Verify that the expiry is set correctly + var expectedExpiry = DateTimeOffset.UtcNow.AddSeconds(expiresIn); + var actualExpiry = typeof(OidcTokenProvider) + .GetField("_expiry", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(auth) as DateTimeOffset?; + actualExpiry.Should().NotBeNull(); + actualExpiry.Value.Should().BeCloseTo(expectedExpiry, precision: TimeSpan.FromSeconds(5)); + + // Verify that the refresh token is set correctly + var actualRefreshToken = typeof(OidcTokenProvider) + .GetField("_refreshToken", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(auth) as string; + actualRefreshToken.Should().NotBeNull(); + actualRefreshToken.Should().Be(refreshToken); + + // Stop the server + server.Stop(); + } + + [Fact] + public async Task TestOidcAuthWithServerError() + { + // Arrange + var server = WireMockServer.Start(); + var idpIssuerUrl = server.Url + "/token"; + var clientId = "CLIENT_ID"; + var clientSecret = "CLIENT_SECRET"; + var expiredIdToken = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjB9.f37LFpIw_XIS5TZt3wdtEjjyCNshYy03lOWpyDViRM0"; + var refreshToken = "REFRESH_TOKEN"; + + // Simulate a server error response + server + .Given(Request.Create().WithPath("/token").UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.InternalServerError) + .WithBody(@"{ ""error"": ""server_error"" }")); + + var auth = new OidcTokenProvider(clientId, clientSecret, idpIssuerUrl, expiredIdToken, refreshToken); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => auth.GetAuthenticationHeaderAsync(CancellationToken.None)); + exception.Message.Should().StartWith("Unable to refresh OIDC token."); + exception.InnerException.Message.Should().Contain("500"); + + // Stop the server + server.Stop(); + } } } From c93fabe07da57c4dc472c46c2770a4ce4f68d776 Mon Sep 17 00:00:00 2001 From: Ayr Loong Date: Sat, 19 Apr 2025 07:55:07 +0800 Subject: [PATCH 4/4] fix typo (#1623) --- src/KubernetesClient/WatcherExt.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/KubernetesClient/WatcherExt.cs b/src/KubernetesClient/WatcherExt.cs index 7b2fe9308..f3047f8ca 100644 --- a/src/KubernetesClient/WatcherExt.cs +++ b/src/KubernetesClient/WatcherExt.cs @@ -11,7 +11,7 @@ public static class WatcherExt /// type of the HttpOperationResponse object /// the api response /// a callback when any event raised from api server - /// a callbak when any exception was caught during watching + /// a callback when any exception was caught during watching /// /// The action to invoke when the server closes the connection. /// @@ -47,7 +47,7 @@ private static Func> MakeStreamReaderCreator(Tasktype of the HttpOperationResponse object /// the api response /// a callback when any event raised from api server - /// a callbak when any exception was caught during watching + /// a callback when any exception was caught during watching /// /// The action to invoke when the server closes the connection. /// @@ -68,7 +68,7 @@ public static Watcher Watch( /// type of the event object /// type of the HttpOperationResponse object /// the api response - /// a callbak when any exception was caught during watching + /// a callback when any exception was caught during watching /// cancellation token /// IAsyncEnumerable of watch events public static IAsyncEnumerable<(WatchEventType, T)> WatchAsync(