Morvael
Morvael

Reputation: 3567

WebAPI2.0 OWIN Token request using JSON

I have created a new WebAPI solution in visual studio and am playing around with the code to try and understand whats going on.

I have a test API thats all up and running with an Authorization controller and another controller that implements all the actual functionality.

The controllers (API) all work by receiving JSON and replying with JSON, with the exception of the /Token request.This has to be:

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

otherwise I just get an error back.

The section of code that creates this endpoint appears to be this:

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
};

Calling it like this results in a 200 Success response, with a Bearer token:

$("#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;
    });
});

Calling it like this results in a 400 Response:

$("#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;
    });
});

The response body is:

{"error":"unsupported_grant_type"}

The only difference here is the encoding used to transmit the request. Every where I look all the examples are using form encoding to request this token.

Placing a breakpoint on the code under /api/Account/ExternalLogin, never gets hit.

Is there a reason for this only accepting form encoding? and if not how can I change the controller to accept JSON?

Alternatively have I just done something stupid?

Upvotes: 16

Views: 9596

Answers (4)

tseshevsky
tseshevsky

Reputation: 801

A customer's tech specialist requested that our /token endpoint could use both "application/x-www-form-urlencoded" and "application/json" formats in body. So I had to implement it, despite it being against the spec.

Create an Owin middleware that converts JSON body into Url-encoded body if path is "/api/token" and content-type is "application/json". Don't forget to register it in 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;
        }
    }

Upvotes: 8

Vlad C
Vlad C

Reputation: 445

OAuth2 requires application/x-www-form-urlencoded content type for token requests.

Still, I thought about this workaround:

    // 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);
    }

Models:

    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; }
    }

It can be improved but works great.

Upvotes: 2

Federico Dipuma
Federico Dipuma

Reputation: 18265

The reason behind the use of application/x-www-form-urlencoded as Content-Type is simple: the OAuth2 specification (RFC 6749) requires this content type for token requests.

Any other content-type will break OAuth2 compliant clients compatibility. I advice you to not change this standard behavior.

Note
Please note that this:

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

works just because you are not sending JSON at all! Even if you added application/json as Content-Type header your request body is serialized as form key-value pairs (the jQuery default object serialization in AJAX calls).

The default implementation of OAuthAuthorizationServerMiddleware (more precisely the internally used OAuthAuthorizationServerHandler) from Microsoft.Owin.Security.OAuth just ignores the Content-Type header and tries to read the request body as a form anyway.

Upvotes: 21

gaurav bhavsar
gaurav bhavsar

Reputation: 2043

No need to JSON.stringify(data) pass data direclty.

Upvotes: 1

Related Questions