8

Using the default Visual Studio 2013 Web API project template with individual user accounts, and posting to the /token endpoint with an Accept header of application/xml, the server still returns the response in JSON:

{"access_token":"...","token_type":"bearer","expires_in":1209599}

Is there a way to get the token back as XML?

CalebG
  • 273
  • 1
  • 3
  • 7

5 Answers5

5

According to RFC6749 the response format should be JSON and Microsoft implemented it accordingly. I found out that JSON formatting is implemented in Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerHandler internal class with no means of extension.

I also encountered the need to have token response in XML. The best solution I came up with was to implement HttpModule converting JSON to XML when stated in Accept header.

public class OAuthTokenXmlResponseHttpModule : IHttpModule
{
    private static readonly string FilterKey = typeof(OAuthTokenXmlResponseHttpModule).Name + typeof(MemoryStreamFilter).Name;

    public void Init(HttpApplication application)
    {
        application.BeginRequest += ApplicationOnBeginRequest;
        application.EndRequest += ApplicationOnEndRequest;
    }

    private static void ApplicationOnBeginRequest(object sender, EventArgs eventArgs)
    {
        var application = (HttpApplication)sender;

        if (ShouldConvertToXml(application.Context.Request) == false) return;

        var filter = new MemoryStreamFilter(application.Response.Filter);
        application.Response.Filter = filter;
        application.Context.Items[FilterKey] = filter;
    }

    private static bool ShouldConvertToXml(HttpRequest request)
    {
        var isTokenPath = string.Equals("/token", request.Path, StringComparison.InvariantCultureIgnoreCase);
        var header = request.Headers["Accept"];

        return isTokenPath && (header == "text/xml" || header == "application/xml");
    }

    private static void ApplicationOnEndRequest(object sender, EventArgs eventArgs)
    {
        var context = ((HttpApplication) sender).Context;

        var filter = context.Items[FilterKey] as MemoryStreamFilter;
        if (filter == null) return;

        var jsonResponse = filter.ToString();
        var xDocument = JsonConvert.DeserializeXNode(jsonResponse, "oauth");
        var xmlResponse = xDocument.ToString(SaveOptions.DisableFormatting);

        WriteResponse(context.Response, xmlResponse);
    }

    private static void WriteResponse(HttpResponse response, string xmlResponse)
    {
        response.Clear();
        response.ContentType = "application/xml;charset=UTF-8";
        response.Write(xmlResponse);
    }

    public void Dispose()
    {
    }
}

public class MemoryStreamFilter : Stream
{
    private readonly Stream _stream;
    private readonly MemoryStream _memoryStream = new MemoryStream();

    public MemoryStreamFilter(Stream stream)
    {
        _stream = stream;
    }

    public override void Flush()
    {
        _stream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return _stream.Read(buffer, offset, count);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        _memoryStream.Write(buffer, offset, count);
        _stream.Write(buffer, offset, count);
    }

    public override string ToString()
    {
        return Encoding.UTF8.GetString(_memoryStream.ToArray());
    }

    #region Rest of the overrides
    public override bool CanRead
    {
        get { throw new NotImplementedException(); }
    }

    public override bool CanSeek
    {
        get { throw new NotImplementedException(); }
    }

    public override bool CanWrite
    {
        get { throw new NotImplementedException(); }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }

    public override long Length
    {
        get { throw new NotImplementedException(); }
    }

    public override long Position
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }
    #endregion
}
Community
  • 1
  • 1
Alexei Matrosov
  • 420
  • 4
  • 10
  • I have implemented your code but its not hitting anywhere when the request raised from postman and it returned it as JSON format. do i missing anything? below is the response which need to return as XML., { "access_token": "CWNgfp66e_aGnYvNDB71hLRrHxXXXXXXXXXXX", "token_type": "bearer", "expires_in": 1209599, "userName": "arunkumaarcn@outlook.com", ".issued": "Tue, 12 May 2020 12:08:34 GMT", ".expires": "Tue, 26 May 2020 12:08:34 GMT" } – King_Fisher May 12 '20 at 12:10
  • @King_Fisher, the OAuthTokenXmlResponseHttpModule should be registered in the request processing pipeline. I believe either programmatically or via Web.config. Sorry, it's been a while, I can't even remember when I needed to implement this :) This should help: https://learn.microsoft.com/en-us/iis/configuration/system.webserver/modules/ – Alexei Matrosov May 13 '20 at 08:40
3

Ok I had such a fun time trying to figure this out using OWIN I thought I would share my solution with the community, I borrowed some insight from other posts https://stackoverflow.com/a/26216511/1148288 and https://stackoverflow.com/a/29105880/1148288 along with the concepts Alexei describs in his post. Nothing fancy doing with implementation but I had a requirement for my STS to return an XML formatted response, I wanted to keep with the paradigm of honoring the Accept header, so my end point would examine that to determine if it needed to run the XML swap or not. This is what I am current using:

private void ConfigureXMLResponseSwap(IAppBuilder app)
{
    app.Use(async (context, next) =>
    {
        if (context.Request != null && 
            context.Request.Headers != null &&
            context.Request.Headers.ContainsKey("Accept") &&
            context.Request.Headers.Get("Accept").Contains("xml"))
        {
            //Set a reference to the original body stream
            using (var stream = context.Response.Body)
            {
                //New up and set the response body as a memory stream which implements the ability to read and set length
                using (var buffer = new MemoryStream())
                {
                    context.Response.Body = buffer;

                    //Allow other middlewares to process
                    await next.Invoke();

                    //On the way out, reset the buffer and read the response body into a string
                    buffer.Seek(0, SeekOrigin.Begin);

                    using (var reader = new StreamReader(buffer))
                    {
                        string responsebody = await reader.ReadToEndAsync();

                        //Using our responsebody string, parse out the XML and add a declaration
                        var xmlVersion = JsonConvert.DeserializeXNode(responsebody, "oauth");
                        xmlVersion.Declaration = new XDeclaration("1.0", "UTF-8", "yes");

                        //Convert the XML to a byte array
                        var bytes = Encoding.UTF8.GetBytes(xmlVersion.Declaration + xmlVersion.ToString());

                        //Clear the buffer bits and write out our new byte array
                        buffer.SetLength(0);
                        buffer.Write(bytes, 0, bytes.Length);
                        buffer.Seek(0, SeekOrigin.Begin);

                        //Set the content length to the new buffer length and the type to an xml type
                        context.Response.ContentLength = buffer.Length;
                        context.Response.ContentType = "application/xml;charset=UTF-8";

                        //Copy our memory stream buffer to the output stream for the client application
                        await buffer.CopyToAsync(stream);
                    }
                }
            }
        }
        else
            await next.Invoke();
    });
}

Of course you would then wire this up during startup config like so:

public void Configuration(IAppBuilder app)
{
    HttpConfiguration httpConfig = new HttpConfiguration();
    //Highly recommend this is first...
    ConfigureXMLResponseSwap(app);
    ...more config stuff...
}

Hope that helps any other lost souls that find there way to the this post seeking to do something like this!

Community
  • 1
  • 1
Lazy Coder
  • 1,157
  • 1
  • 8
  • 18
  • Thank for sharing Your answer! I had similar problem (http://stackoverflow.com/questions/36647419/asp-net-identity-2-custom-response-from-oauthauthorizationserverprovider) and after I found Your answer I was able to solve it using custom middleware. This should be accepted answer! – Misiu Apr 25 '16 at 13:44
  • I hope you didn't put this exact code into production because it is fairly buggy. Apart from that, good solution (although Alexei's proposal is equally good). – hbulens May 12 '16 at 13:10
  • @hbulens care to elaborate? We had 1 specific use case for this and it isn't in production yet, but I would like to get any feedback if you care to share. – Lazy Coder May 12 '16 at 16:11
  • @LazyCoder For starters, lots of null reference exceptions in your first line of code. Add some safety checks (use the ? operator and you'll be fine). And you also don't dispose your streams, is that done for a certain reason? – hbulens May 12 '16 at 16:40
  • I see your point, for some reason I was thinking the framework would handle the stream disposal since this is a web request but I guess in this case that is not true? I'll refactor and update the code. – Lazy Coder May 12 '16 at 17:03
0

take a look here i hope it can help how to set a Web API REST service to always return XML not JSON

makemoney2010
  • 1,222
  • 10
  • 11
  • I have: GlobalConfiguration.Configuration.Formatters.XmlFormatter.UseDataContractSerializer = true; The regular endpoints of my controllers all honor Accept: application/xml. It's only the default /token endpoint that does not seem to – CalebG Apr 29 '14 at 23:37
  • at this point i don't know how to help you.I've to googling to find a possible solution. – makemoney2010 Apr 29 '14 at 23:41
0

Could you retry by doing the following steps:

In the WebApiConfig.Register(), specify

    config.Formatters.XmlFormatter.UseXmlSerializer = true;

    var supportedMediaTypes = config.Formatters.XmlFormatter.SupportedMediaTypes;

    if (supportedMediaTypes.Any(it => it.MediaType.IndexOf("application/xml", StringComparison.InvariantCultureIgnoreCase) >= 0) ==false)
    {
        supportedMediaTypes.Insert(0,new MediaTypeHeaderValue("application/xml"));

    }
Toan Nguyen
  • 11,263
  • 5
  • 43
  • 59
  • Tried it, but the /token endpoint still returns JSON. The default ValuesController that comes with the project template returns XML when the Accept header is application/xml. Debugging it, the above if-statement evaluated to false. – CalebG Apr 30 '14 at 17:16
  • Could you please add the screenshots of your request and response? It doubt that there is a handler of your server which prevents XML from being returned. – Toan Nguyen Apr 30 '14 at 23:43
-1

I normally just remove the XmlFormatter altogether.

// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);

Add the line above in your WebApiConfig class...

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

        // Web API routes
        config.MapHttpAttributeRoutes();

        // Remove the XML formatter
        config.Formatters.Remove(config.Formatters.XmlFormatter);

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
Karthik Murugesan
  • 1,100
  • 3
  • 13
  • 25