3

I have implemented a custom OAuthAuthorizationServerProvider and I want to add some additional elements in the response when my client is requesting an access token.

To do so I overrided the OAuthAuthorizationServerProvider.TokenEndpoint method and I successfully managed to add some single elements (by adding them into the context.AdditionalResponseParameters dictionary). Now I have this kind of response:

{
  "access_token": "wxoCtLSdPXNW9KK09PVhSqYho...",
  "token_type": "bearer",
  "expires_in": 1199,
  "refresh_token": "uk0kFyj4Q2OufWKt4IzWQHlj...",
  "toto": "bloblo",
  "tata": "blabla"
}

This is great but my goal here is to add an array in order to get this kind of response:

{
  "access_token": "wxoCtLSdPXNW9KK09PVhSqYho...",
  "token_type": "bearer",
  "expires_in": 1199,
  "refresh_token": "uk0kFyj4Q2OufWKt4IzWQHlj...",
  "scopes": ["read", "write"]
}

I tried to add a json-parsed list or array instead of a simple string but it gives me

"scopes": "[\"read\",\"write\"]"

That's the string parsed into a Json, not a Json array :/

How can I add a Json array in the TokenEndpoint response?

Quentin V.
  • 345
  • 2
  • 13
  • 2
    That seems to be not possible, because of the way AdditionalResponseParameters are handled (JsonTextWriter.WriteValue will be used for each paramter, and this method expects only primitive values like numbers, guids, strings and so on, not arrays or objects). – Evk Nov 28 '16 at 10:58
  • Oh, well ok then I'll find a way to make it clear with only strings. Thanks Evk – Quentin V. Nov 28 '16 at 14:45

1 Answers1

2

Problem

When we use app.OAuthBearerAuthenticationExtensions, the next chain is called:

public static class OAuthBearerAuthenticationExtensions
  {
    public static IAppBuilder UseOAuthBearerAuthentication(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
    {
      if (app == null)
        throw new ArgumentNullException(nameof (app));
      app.Use((object) typeof (OAuthBearerAuthenticationMiddleware), (object) app, (object) options);
      app.UseStageMarker(PipelineStage.Authenticate);
      return app;
    }
  }

Then an object of type OAuthAuthorizationServerMiddleware uses internal class OAuthAuthorizationServerHandler where JsonTextWriter is used:

using (var jsonTextWriter = new JsonTextWriter((TextWriter) new StreamWriter((Stream) memory)))
{
    jsonTextWriter.WriteStartObject();
    jsonTextWriter.WritePropertyName("access_token");
    jsonTextWriter.WriteValue(accessToken);
    jsonTextWriter.WritePropertyName("token_type");
    jsonTextWriter.WriteValue("bearer");
    // and so on
    this.Response.ContentLength = new long?((long) body.Length);
    await this.Response.WriteAsync(body, this.Request.CallCancelled);
}

There are two limitations here:
*) JsonTextWriter is a pure class that cannot be configured, it just writes string as StringBuilder, so Json.Settings = new MySettings() cannot be applied. Also JsontTextWriter does not support complex objects. Arrays can be only written as jsonTextWriter.WriteStartArray() and jsonTextWriter.WriteEndArray(), but this is ignored in the OAuthAuthorizationServerHandler.
*) Some classes are internal and cannot be overwritten or inherited.

It seems that Microsoft developers did not foresee this problem and just limited custom properties to IDictionary<string, string>.

Solution 1

Instead of app.UseOAuthBearerAuthentication(...) apply your own code

app.Use<MyOAuthBearerAuthenticationMiddleware>(options);
app.UseStageMarker(PipelineStage.Authenticate);

You can derive a class from OAuthBearerAuthenticationMiddleware and use for your own purposes.

Solution 2

Override the token Endpoint response. This is a tricky thing.

1) Create a custom middleware that will wrap other calls and override Body response stream.

class AuthenticationPermissionsMiddleware : OwinMiddleware
{
    public AuthenticationPermissionsMiddleware(OwinMiddleware next) 
        : base(next)
    {            
    }

    public override async Task Invoke(IOwinContext context)
    {
        if (!context.Request.Path.Equals("/Token")
        {
            await Next.Invoke(context);
            return;
        }

        using (var tokenBodyStream = new MemoryStream())
        {
            // save initial OWIN stream
            var initialOwinBodyStream = context.Response.Body;

            // create new memory stream
            context.Response.Body = tokenBodyStream;

            // other middlewares will will update our tokenBodyStream
            await Next.Invoke(context);

            var tokenResponseBody = GetBodyFromStream(context.Response);
            var obj = JsonConvert.DeserializeObject(tokenResponseBody);
            var jObject = JObject.FromObject(obj);

            // add your custom array or any other object
            var scopes = new Scope[];
            jObject.Add("scopes", JToken.FromObject(scopes));
            var bytes = Encoding.UTF8.GetBytes(jObject.ToString());
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            await tokenBodyStream.WriteAsync(bytes, 0, bytes.Length);

            context.Response.ContentLength = data.LongLength;
            tokenBodyStream.Seek(0, SeekOrigin.Begin);

            // get back result to the OWIN stream
            await context.Response.Body.CopyToAsync(initialOwinBodyStream);
            }
        }
    }

    private string GetBodyFromStream(IOwinResponse response)
    {
        using (var memoryStream = new MemoryStream())
        {
            response.Body.Seek(0, SeekOrigin.Begin);
            response.Body.CopyTo(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(memoryStream))
            {
                return reader.ReadToEnd();
            }
        }
    }
}

2) Use the new middleware before UseOAuthBearerTokens in the authentication startup method.

app.Use<AuthenticationPermissionsMiddleware>();
app.UseOAuthBearerTokens(options);
Artur A
  • 7,115
  • 57
  • 60