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(); + } } }