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?

2
  • 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). Commented Nov 28, 2016 at 10:58
  • Oh, well ok then I'll find a way to make it clear with only strings. Thanks Evk Commented Nov 28, 2016 at 14:45

1 Answer 1

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);
Sign up to request clarification or add additional context in comments.

1 Comment

This is the only solution, if you want to return custom response, Well explained.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.