oauth-2.0 C# - FormUrlEncodedContent将空格编码到'%20'而不是'+'

aamkag61  于 2022-10-31  发布在  C#
关注(0)|答案(2)|浏览(232)

我正在尝试使用Client credential grant(https://discord.com/developers/docs/topics/oauth2#client-credentials-grant)连接到Discord的开放身份验证端点
Discord要求将scope作为urlencoded字符串发送:identify%20email%20guilds
默认情况下,C# HttpClient似乎将空格转换为+,而不是%20
以下代码

var scopeasStr = string.Join(" ", opts.Scopes);
            //scopeasStr = HttpUtility.UrlEncode(scopeasStr);
            //scopeasStr = Uri.EscapeDataString(scopeasStr);

            var nvc = new List<KeyValuePair<string, string>>();
            nvc.Add(new KeyValuePair<string, string>("grant_type", opts.GrantType));
            nvc.Add(new KeyValuePair<string, string>("scope", scopeasStr));

            var content = new FormUrlEncodedContent(nvc);
            var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{apiUrl}/oauth2/token");
            requestMessage.Content = content;

            var response = await externalHttpClient.SendAsync(requestMessage);

生成以下请求

POST https://discord.com/api/v10/oauth2/token HTTP/1.1
Host: discord.com
Authorization: Basic VerySecret
Content-Type: application/x-www-form-urlencoded
Content-Length: 57

grant_type=client_credentials&scope=identify+guilds+email

返回一个400 Bad Request

{"error": "invalid_scope", "error_description": "The requested scope is invalid, unknown, or malformed."}

我试着用scopeasStr = Uri.EscapeDataString(scopeasStr);来编码这个值,但是%20httpClient编码成了%2520

POST https://discord.com/api/v10/oauth2/token HTTP/1.1
Host: discord.com
Authorization: Basic VerySecret
Content-Type: application/x-www-form-urlencoded
Content-Length: 65

grant_type=client_credentials&scope=identify%2520guilds%2520email
  • 当只发送一个作用域时,请求有效。所以肯定是作用域中的空间导致了这个问题。我已经向Discord支持部门确认了他们只接受%20来分隔作用域。
  • 我应该如何使用HttpClient正确地对此进行编码?

完整C#代码:

public static async Task Authenticate(this HttpClient client, AuthenticateOptions opts, bool forceNew = false)
        {
            if (client.DefaultRequestHeaders.Authorization == null || forceNew)
            {
                var externalHttpClient = new HttpClient();

                var apiUrl = opts.EndPointUrl;
                var clientId = opts.ClientId;
                var secret = opts.ClientSecret;
                var scopeasStr = string.Join(" ", opts.Scopes);
                //scopeasStr = HttpUtility.UrlEncode(scopeasStr);
                //scopeasStr = Uri.EscapeDataString(scopeasStr);
                externalHttpClient.BaseAddress = new Uri(apiUrl);

                var nvc = new List<KeyValuePair<string, string>>();
                nvc.Add(new KeyValuePair<string, string>("grant_type", opts.GrantType));

                nvc.Add(new KeyValuePair<string, string>("scope", scopeasStr));
                var content = new FormUrlEncodedContent(nvc);
                var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{apiUrl}/oauth2/token");
                requestMessage.Content = content;
                var authenticationString = $"{clientId}:{secret}";
                var base64EncodedAuthenticationString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString));
                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString);

                var response = await externalHttpClient.SendAsync(requestMessage);
                if (response.IsSuccessStatusCode)
                {
                    var value = await response.ParseResponse();
                    var json = JObject.Parse(value);
                    json.TryGetValue("access_token", out var v);
                    var accessToken = v.Value<string>();
                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                }
                else
                {
                    var value = await response.ParseResponse();
                    Console.Write($"{response.StatusCode} - {value}");
                    Assert.Fail();
                }

            }
        }
flseospp

flseospp1#

基本上,url编码所做的就是将字符串中的一些字符编码为其他预定义的常量。这是基本的替换。我相信你可以创建自己的urlencoding方法,并将其编码为%20,而不是编码为加号。因为%20确实是空格的正确urlencoded等价物。

gk7wooem

gk7wooem2#

x1c 0d1x发现有一个替换,它将FormUrlEncodedContent中的+替换为%20。最终生成了我自己的ByteArrayContent/FormUrlEncodedContent版本。

public class CustomFormUrlEncodedContent : ByteArrayContent
{
        public CustomFormUrlEncodedContent(
            IEnumerable<KeyValuePair<string, string>> nameValueCollection)
            : base(GetContentByteArray(nameValueCollection))
        {
            Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
        }

        private static byte[] GetContentByteArray(IEnumerable<KeyValuePair<string?, string?>> nameValueCollection)
        {
            if (nameValueCollection == null)
            {
                throw new ArgumentNullException(nameof(nameValueCollection));
            }

            // Encode and concatenate data
            StringBuilder builder = new StringBuilder();
            foreach (KeyValuePair<string?, string?> pair in nameValueCollection)
            {
                if (builder.Length > 0)
                {
                    builder.Append('&');
                }

                builder.Append(Encode(pair.Key));
                builder.Append('=');
                builder.Append(Encode(pair.Value));
            }

            return Encoding.GetEncoding(28591).GetBytes(builder.ToString());
        }

        private static string Encode(string? data)
        {
            if (string.IsNullOrEmpty(data))
            {
                return string.Empty;
            }
        // Escape spaces as '+'.
        return Uri.EscapeDataString(data);//.Replace("%20", "+");
        }
}

相关问题