使用 JSON 的 WebAPI2.0 OWIN 令牌请求

WebAPI2.0 OWIN Token request using JSON

我在 visual studio 中创建了一个新的 WebAPI 解决方案,并且正在研究代码以尝试了解发生了什么。

我有一个测试 API 全部完成,运行 一个授权控制器和另一个实现所有实际功能的控制器。

控制器 (API) 都通过接收 JSON 并回复 JSON 来工作,除了 /Token request.This 必须是:

Content-Type: application/x-www-form-urlencoded

否则我只会返回一个错误。

创建此端点的代码部分似乎是这样的:

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // In production mode set AllowInsecureHttp = false
    AllowInsecureHttp = false
};

像这样调用它会导致 200 成功响应,带有 Bearer 令牌:

$("#token_button").click(function ()
{
    var username = $("#token_email").val();
    var password = $("#token_password").val();

    postData("Token", "grant_type=password&username=" + username + "&password=" + password, "application/x-www-form-urlencoded", function (data)
    {
        user = data;
        $("#feedback_display").html(user.access_token);
    }, function ()
    {
        user = null;
    });
});

像这样调用它会导致 400 响应:

$("#token_button").click(function ()
{
    var username = $("#token_email").val();
    var password = $("#token_password").val();

    var data = {
        "grant_type": "password",
        "username": username,
        "password": password
    }

    postData("Token", JSON.stringify(data), "application/json", function (data)
    {
        user = data;
        $("#feedback_display").html(user.access_token);
    }, function ()
    {
        user = null;
    });
});

响应正文是:

{"error":"unsupported_grant_type"}

这里唯一的区别是用于传输请求的编码。 在我看到的每个地方,所有示例都使用表单编码来请求此令牌。

在 /api/Account/ExternalLogin 下的代码上放置断点,永远不会命中。

这只接受表单编码有什么原因吗?如果不能,我该如何更改控制器以接受 JSON?

或者我刚刚做了什么蠢事?

不需要[=10th=]直接传递数据。

application/x-www-form-urlencoded 用作 Content-Type 的原因很简单:OAuth2 specification (RFC 6749) 需要此内容类型来进行令牌请求。

任何其他 content-type 都会破坏 OAuth2 兼容的客户端兼容性。我建议您不要更改此标准行为。

备注
请注意:

postData("Token", data, "application/json", function (data)
{
    //...
}

有效只是因为您根本没有发送JSON!即使您将 application/json 添加为 Content-Type header,您的请求 body 也会序列化为形式 key-value 对(jQuery 默认 object AJAX 调用中的序列化)。

OAuthAuthorizationServerMiddleware(更准确地说是内部使用的 OAuthAuthorizationServerHandler)来自 Microsoft.Owin.Security.OAuth 的默认实现只是忽略 Content-Type header 并尝试读取无论如何请求 body 作为表格。

OAuth2 需要 application/x-www-form-urlencoded 令牌请求的内容类型。

不过,我想到了这个解决方法:

    // GET api/Account/GetToken
    [HttpPost]
    [AllowAnonymous]
    [Route("GetToken")]
    public async Task<IHttpActionResult> GetToken(TokenRequest request)
    {
        var client = new HttpClient()
        {
            BaseAddress = new Uri(Request.RequestUri.GetLeftPart(UriPartial.Authority))
        };

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "password"),
            new KeyValuePair<string, string>("username", request.Username),
            new KeyValuePair<string, string>("password", request.Password)
        });

        var result = await client.PostAsync("/token", content);
        string resultContent = await result.Content.ReadAsStringAsync();
        resultContent = resultContent.Replace(".issued", "issued").Replace(".expires", "expires");
        TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(resultContent);

        return Ok(tokenResponse);
    }

型号:

    public class TokenRequest
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }

    public class TokenResponse
    {
        public string access_token { get; set; }
        public string token_type { get; set; }
        public int expires_in { get; set; }
        public string userName { get; set; }
        public DateTime issued { get; set; }
        public DateTime expires { get; set; }
        public string error { get; set; }
        public string error_description { get; set; }
    }

它可以改进,但效果很好。

客户的技术专家要求我们的 /token 端点可以在正文中使用 "application/x-www-form-urlencoded" 和 "application/json" 格式。 所以我不得不实施它,尽管它违反了规范。

如果路径为“/api/token”且 content-type 为 "application/json",则创建一个将 JSON 正文转换为 Url-encoded 正文的 Owin 中间件。不要忘记在 Startup.cs.

中注册它
public sealed class JsonBodyToUrlEncodedBodyMiddleware : OwinMiddleware
    {
        public JsonBodyToUrlEncodedBodyMiddleware(OwinMiddleware next)
            : base(next)
        {
        }

        public override async Task Invoke(IOwinContext context)
        {
            if (string.Equals(context.Request.ContentType, "application/json")
                && string.Equals(context.Request.Method, "POST", StringComparison.InvariantCultureIgnoreCase)
                && context.Request.Path == new PathString("/avi/token/"))
            {
                try
                {
                    await ReplaceJsonBodyWithUrlEncodedBody(context);
                    await Next.Invoke(context);
                }
                catch (Exception)
                {
                    context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
                    context.Response.Write("Invalid JSON format.");
                }
            }
            else
            {
                await Next.Invoke(context);
            }
        }

        private async Task ReplaceJsonBodyWithUrlEncodedBody(IOwinContext context)
        {
            var requestParams = await GetFormCollectionFromJsonBody(context);
            var urlEncodedParams = string.Join("&", requestParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
            var decryptedContent = new StringContent(urlEncodedParams, Encoding.UTF8, "application/x-www-form-urlencoded");
            var requestStream = await decryptedContent.ReadAsStreamAsync();
            context.Request.Body = requestStream;
        }

        private static async Task<Dictionary<string, string>> GetFormCollectionFromJsonBody(IOwinContext context)
        {
            context.Request.Body.Position = 0;
            var jsonString = await new StreamReader(context.Request.Body).ReadToEndAsync();
            var requestParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
            return requestParams;
        }
    }