From fe7863f2ccd133c1629a01d605d37cb588c2a3fb Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Fri, 21 Feb 2025 01:26:59 -0800 Subject: [PATCH 1/3] Refactor OidcTokenProvider to remove dependency on IdentityModel and improve token handling --- .../Authentication/OidcTokenProvider.cs | 94 +++++++++++++------ src/KubernetesClient/KubernetesClient.csproj | 2 - 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/KubernetesClient/Authentication/OidcTokenProvider.cs b/src/KubernetesClient/Authentication/OidcTokenProvider.cs index ef9c35403..0b8aa404b 100644 --- a/src/KubernetesClient/Authentication/OidcTokenProvider.cs +++ b/src/KubernetesClient/Authentication/OidcTokenProvider.cs @@ -1,23 +1,32 @@ -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(); + + if (!string.IsNullOrEmpty(_idToken)) + { + _expiry = GetExpiryFromToken(); + } } public async Task GetAuthenticationHeaderAsync(CancellationToken cancellationToken) @@ -30,49 +39,78 @@ public async Task GetAuthenticationHeaderAsync(Cancel return new AuthenticationHeaderValue("Bearer", _idToken); } - private DateTime getExpiryFromToken() + private DateTimeOffset GetExpiryFromToken() { - long expiry; - var handler = new JwtSecurityTokenHandler(); - try + var parts = _idToken.Split('.'); + if (parts.Length != 3) { - var token = handler.ReadJwtToken(_idToken); - expiry = token.Payload.Expiration ?? 0; + throw new ArgumentException("Invalid JWT token format."); } - catch + + 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)) { - expiry = 0; + var exp = expElement.GetInt64(); + var expiryDateTime = DateTimeOffset.FromUnixTimeSeconds(exp); + return expiryDateTime; + } + else + { + throw new ArgumentException("JWT token does not contain 'exp' claim."); } - - return DateTimeOffset.FromUnixTimeSeconds(expiry).UtcDateTime; } - 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)) { - throw new Exception(result.Error); + _idToken = idTokenElement.GetString(); } - _idToken = result.IdentityToken; - _refreshToken = result.RefreshToken; - _expiry = result.AccessTokenExpiration; + if (jsonDocument.RootElement.TryGetProperty("refresh_token", out var refreshTokenElement)) + { + _refreshToken = refreshTokenElement.GetString(); + } + + 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 @@ - - From dffc59e5cefa691b146c5780874358ad85495e2e Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Fri, 21 Feb 2025 01:48:20 -0800 Subject: [PATCH 2/3] 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. --- .../Authentication/OidcTokenProvider.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/KubernetesClient/Authentication/OidcTokenProvider.cs b/src/KubernetesClient/Authentication/OidcTokenProvider.cs index 0b8aa404b..1826928e6 100644 --- a/src/KubernetesClient/Authentication/OidcTokenProvider.cs +++ b/src/KubernetesClient/Authentication/OidcTokenProvider.cs @@ -22,11 +22,7 @@ public OidcTokenProvider(string clientId, string clientSecret, string idpIssuerU _idpIssuerUrl = idpIssuerUrl; _idToken = idToken; _refreshToken = refreshToken; - - if (!string.IsNullOrEmpty(_idToken)) - { - _expiry = GetExpiryFromToken(); - } + _expiry = GetExpiryFromToken(); } public async Task GetAuthenticationHeaderAsync(CancellationToken cancellationToken) @@ -44,24 +40,28 @@ private DateTimeOffset GetExpiryFromToken() var parts = _idToken.Split('.'); if (parts.Length != 3) { - throw new ArgumentException("Invalid JWT token format."); + return default; } - 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)) + try { - var exp = expElement.GetInt64(); - var expiryDateTime = DateTimeOffset.FromUnixTimeSeconds(exp); - return expiryDateTime; + 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); + } } - else + catch { - throw new ArgumentException("JWT token does not contain 'exp' claim."); + // ignore to default } + + return default; } private static byte[] Base64UrlDecode(string input) From c253b82b158fa3bd4f578558f446bb8b477ffba6 Mon Sep 17 00:00:00 2001 From: Boshi Lian Date: Fri, 21 Feb 2025 02:15:25 -0800 Subject: [PATCH 3/3] 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. --- src/KubernetesClient/Authentication/OidcTokenProvider.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/KubernetesClient/Authentication/OidcTokenProvider.cs b/src/KubernetesClient/Authentication/OidcTokenProvider.cs index 1826928e6..912ea0fde 100644 --- a/src/KubernetesClient/Authentication/OidcTokenProvider.cs +++ b/src/KubernetesClient/Authentication/OidcTokenProvider.cs @@ -37,14 +37,9 @@ public async Task GetAuthenticationHeaderAsync(Cancel private DateTimeOffset GetExpiryFromToken() { - var parts = _idToken.Split('.'); - if (parts.Length != 3) - { - return default; - } - try { + var parts = _idToken.Split('.'); var payload = parts[1]; var jsonBytes = Base64UrlDecode(payload); var json = Encoding.UTF8.GetString(jsonBytes);