Skip to content

Middleware does not pass OwinContext.User forward in Pipeline #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
daniefer opened this issue Jan 19, 2018 · 13 comments
Closed

Middleware does not pass OwinContext.User forward in Pipeline #119

daniefer opened this issue Jan 19, 2018 · 13 comments
Assignees

Comments

@daniefer
Copy link

I am trying to setup a web api with signalr and during the process I noticed this oddity. I cannot seem to find a documented reason why the Identity on the OwinContext would be emptied out after there was no match on the web api route table. I put together a simple project to show this in action:

Startup

public void Configuration(IAppBuilder appBuilder)
{
    var config = new HttpConfiguration();
    config.Filters.Clear();
    config.SuppressDefaultHostAuthentication();
    appBuilder.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
    {
        AuthenticationMode = AuthenticationMode.Active,
        AccessTokenFormat = new TokenFormatter(),
        AuthenticationType = "DummyAuth",
        Provider = new OAuthBearerAuthenticationProvider
        {
            OnValidateIdentity = async (ctx) => ctx.Validated(ctx.Ticket),
            OnRequestToken = async (ctx) => ctx.Token = "111"
        }
    });
    appBuilder.Use<DebugMiddleware>("New Request", (Action<IOwinContext>)((IOwinContext ctx) => Console.WriteLine("End Request")));
    appBuilder.UseCors(CorsOptions.AllowAll);
    appBuilder.Use<DebugMiddleware>("Before WebApi");
    config.MapHttpAttributeRoutes();
    appBuilder.UseWebApi(config);
    appBuilder.Use<DebugMiddleware>("After WebApi");
    appBuilder.RunSignalR();
    appBuilder.Use<DebugMiddleware>("After SignalR");
    config.EnsureInitialized();
}

Dummy token formatter

public class TokenFormatter : ISecureDataFormat<AuthenticationTicket>
{
    public string Protect(AuthenticationTicket data)
    {
        return "111";
    }
    public AuthenticationTicket Unprotect(string protectedText)
    {
        var identity = new ClaimsIdentity("DummyAuth");
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob"));
        identity.AddClaim(new Claim(ClaimTypes.Name, "Bob"));
        var ticket = new AuthenticationTicket(identity, null);
        return ticket;
    }
}

Controller

[Authorize]
public class TestController : ApiController
{
    [Route("Value")]
    [HttpGet]
    public string GetValue()
    {
        return "Hello World!";
    }
}

Hub

public class TestHub : Hub
{
    public override Task OnConnected()
    {
        Console.WriteLine($"Hub.OnConnected Username: {new OwinContext(Context.Request.Environment).Authentication?.User?.Identity?.Name}");
        return base.OnConnected();
    }
    public override Task OnDisconnected(bool stopCalled)
    {
        Console.WriteLine($"Hub.OnDisconnected Username: {new OwinContext(Context.Request.Environment).Authentication?.User?.Identity?.Name}");
        return base.OnDisconnected(stopCalled);
    }
}

If I attempt to connect to a Hub (at the end of the pipeline) the OwinContext is no longer authenticated.
The output from each DebugMiddleware shows:

Output

Start Request
Authenticated?: True
User: Bob
Before WebApi
Authenticated?: True
User: Bob
After WebApi
Authenticated?: False <- Why the change here?
User:
End Request

Is this a bug or is there a reason for this maddening quirk?

@mkArtakMSFT
Copy link
Member

mkArtakMSFT commented Jan 25, 2018

Hi @daniefer. Thanks for all the details you've provided. Do you happen to have a small sample project we can use to repro the issue? //cc @danroth27, @rynowak

@daniefer
Copy link
Author

Cleaned up the example a bit and made a repo here https://github.com/daniefer/AspWebApiIssueExample. Thanks @mkArtakMSFT for getting this started.

@daniefer
Copy link
Author

daniefer commented Feb 8, 2018

Any update or ideas @mkArtakMSFT / @danroth27 / @rynowak ?

@mkArtakMSFT
Copy link
Member

@dougbu, do you have any thoughts about this?

@dougbu
Copy link
Member

dougbu commented Feb 9, 2018

Clearing the IOwinContext.User property is mainly the result of calling HttpConfiguration.SuppressDefaultHostAuthentication() in the Startup class. That method adds the PassiveAuthenticationMessageHandler which (in part) sets OwinHttpRequestContext.Principal to the anonymous user. That Principal property writes through to IOwinContext.User.

Workaround

The test application seems to work as expected if I remove the SuppressDefaultHostAuthentication() call.

If you need that call, I suggest creating a new message handler (DelegatingHandler subclass) to wrap PassiveAuthenticationMessageHandler (i.e. use PassiveAuthenticationMessageHandler as the InnerHandler) and inserting that handler instead of calling SuppressDefaultHostAuthentication(). The handler should save IOwinContext.User and restore it after the usual base.SendAsync(...) call.

Potential fix

Changing PassiveAuthenticationMessageHandler to restore HttpRequestContext.Principal after its base.SendAsync(...) call seems like the obvious fix. But, the code may be as it is for a reason i.e. not just because we were thinking Web API would always be at the end of the Owin pipeline. That is, we may need to add a configuration switch to opt into (or out of) the new behaviour to avoid a breaking change.

@dougbu
Copy link
Member

dougbu commented Feb 9, 2018

Don't need much more investigation. But, I'm leaving the investigate label 'til we discuss whether my suggested fix is a breaking change.

BTW we should change SuppressHostPrincipalMessageHandler if we update PassiveAuthenticationMessageHandler. That also sets but does not restore HttpRequestContext.Principal.

@abatishchev
Copy link
Contributor

Adding public bool RestorePrincipal { get; set; } which naturally will be by default false sounds to me as the right approach.

@dougbu
Copy link
Member

dougbu commented Feb 9, 2018

We discussed this internally and decided two things:

  1. Many, likely most, Web API scenarios will be totally unaffected if we change PassiveAuthenticationMessageHandler and SuppressHostPrincipalMessageHandler to restore the Principal before SendAsync(...) returns. Must both use an Owin host and perform authentication within the Owin pipeline (not the inner Web API pipeline) to see this issue.
    Admittedly, could see something similar with another custom host that has an independent "outer" pipeline of its own.
  2. It's Just Wrong:tm: not to restore the Principal.

Therefore, we should fix this problem without a fallback to the previous, incorrect behaviour.

@dougbu dougbu removed their assignment Feb 9, 2018
@dougbu dougbu removed the investigate label Feb 9, 2018
@dougbu
Copy link
Member

dougbu commented Feb 9, 2018

Clearing assignment and investigate label. Over to the triage team…

@daniefer
Copy link
Author

daniefer commented Feb 15, 2018

Thanks for investigation everyone. It is good to hear that I am not losing my mind.

Another bit of info that might be useful, I was able to work around this without adding a wrapper around the PassiveAuthenticationMessageHandler. The issue does not appear if I put the web api on a sub route, e.g.:

   appBuilder.Map("api" app => {
      app.UseWebApi(config);
   };
   appBuilder.RunSignalR();

Luckily we were already prefixing all our routes with a consistent name so it was a pretty simple transition.

@mkArtakMSFT
Copy link
Member

Thanks @dougbu , @daniefer. I'm closing this issue as it seems the workaround was pretty cheap to apply.

@mkArtakMSFT
Copy link
Member

Reopening, as the actual fix is very cheap too... Better to do it and forget!

@dougbu
Copy link
Member

dougbu commented Feb 22, 2018

f9e06d6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants