Skip to content

Commit b56455a

Browse files
committed
fix(csharp): use runtime .NET version detection for query param URL-encoding in HTTP signature
The HttpSigningConfiguration template used the Mustache conditional net90OrLater to decide whether to URL-encode query parameter keys. When targeting net8.0, this conditional evaluates to false and the UrlEncode call is omitted entirely, producing an unencoded key in the signature base string. However, RestSharp 112+ always sends URL-encoded query parameters on the wire, causing a signature mismatch and HTTP 401 on every request that contains special characters in query parameter names (e.g. OData \, \, \). Replace the compile-time Mustache conditional with runtime detection using RuntimeInformation.FrameworkDescription. On .NET 9+ the key is left as-is (ParseQueryString already encodes internally); on .NET 8 and earlier, HttpUtility.UrlEncode is called explicitly so the signature matches the actual request.
1 parent fc60f97 commit b56455a

15 files changed

Lines changed: 180 additions & 30 deletions

File tree

modules/openapi-generator/src/main/resources/csharp/HttpSigningConfiguration.mustache

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ namespace {{packageName}}.Client
2525
{
2626
HashAlgorithm = HashAlgorithmName.SHA256;
2727
SigningAlgorithm = "PKCS1-v15";
28+
string framework = RuntimeInformation.FrameworkDescription;
29+
_skipUrlEncode = framework.StartsWith(".NET ") &&
30+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
2831
}
2932

3033
/// <summary>
@@ -67,6 +70,14 @@ namespace {{packageName}}.Client
6770
/// </summary>
6871
public int SignatureValidityPeriod { get; set; }
6972

73+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
74+
// so calling UrlEncode again would cause double-encoding and produce a signature
75+
// that does not match the actual request sent by RestSharp 112+.
76+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
77+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
78+
// in the signature as they are in the outgoing HTTP request.
79+
private readonly bool _skipUrlEncode;
80+
7081
private enum PrivateKeyType
7182
{
7283
None = 0,
@@ -133,8 +144,7 @@ namespace {{packageName}}.Client
133144
foreach (var parameter in requestOptions.QueryParameters)
134145
{
135146
#if (NETCOREAPP)
136-
string framework = RuntimeInformation.FrameworkDescription;
137-
string key = framework.StartsWith(".NET 9") ? parameter.Key : {{#net90OrLater}}HttpUtility.UrlEncode({{/net90OrLater}}parameter.Key{{#net90OrLater}}){{/net90OrLater}};
147+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
138148
if (parameter.Value.Count > 1)
139149
{ // array
140150
foreach (var value in parameter.Value)

samples/client/petstore/csharp/httpclient/net10/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/httpclient/net9/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/httpclient/standard2.0/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : parameter.Key;
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net10/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net4.7/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : parameter.Key;
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net4.8/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : parameter.Key;
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net8/EnumMappings/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : parameter.Key;
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net8/Petstore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : parameter.Key;
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

samples/client/petstore/csharp/restsharp/net9/EnumMappings/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public HttpSigningConfiguration()
3333
{
3434
HashAlgorithm = HashAlgorithmName.SHA256;
3535
SigningAlgorithm = "PKCS1-v15";
36+
string framework = RuntimeInformation.FrameworkDescription;
37+
_skipUrlEncode = framework.StartsWith(".NET ") &&
38+
int.TryParse(framework.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
3639
}
3740

3841
/// <summary>
@@ -75,6 +78,14 @@ public HttpSigningConfiguration()
7578
/// </summary>
7679
public int SignatureValidityPeriod { get; set; }
7780

81+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
82+
// so calling UrlEncode again would cause double-encoding and produce a signature
83+
// that does not match the actual request sent by RestSharp 112+.
84+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
85+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
86+
// in the signature as they are in the outgoing HTTP request.
87+
private readonly bool _skipUrlEncode;
88+
7889
private enum PrivateKeyType
7990
{
8091
None = 0,
@@ -141,8 +152,7 @@ public Dictionary<string, string> GetHttpSignedHeader(string basePath,string met
141152
foreach (var parameter in requestOptions.QueryParameters)
142153
{
143154
#if (NETCOREAPP)
144-
string framework = RuntimeInformation.FrameworkDescription;
145-
string key = framework.StartsWith(".NET 9") ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
155+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
146156
if (parameter.Value.Count > 1)
147157
{ // array
148158
foreach (var value in parameter.Value)

0 commit comments

Comments
 (0)