Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web API Return OAuth Token as XML

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?

like image 488
CalebG Avatar asked Apr 29 '14 21:04

CalebG


People also ask

Does OAuth use XML?

Security Assertion Markup Language (SAML) and Open Authorization (OAuth) have emerged as the go-to technologies for federated authentication. While SAML is an Extensible Markup Language (XML)-based standard, OAuth is based on JavaScript Object Notation (JSON), binary, or even SAML formats.


2 Answers

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
}
like image 194
Alexei Matrosov Avatar answered Oct 19 '22 05:10

Alexei Matrosov


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!

like image 37
Lazy Coder Avatar answered Oct 19 '22 06:10

Lazy Coder