User (IPrincipal) not available on ApiController's constructor using Web Api 2.1 and Owin

五迷三道 提交于 2019-11-27 21:21:45

问题


I am Using Web Api 2.1 with Asp.Net Identity 2. I am trying to get the authenticated User on my ApiController's constructor (I am using AutoFac to inject my dependencies), but the User shows as not authenticated when the constructor is called.

I am trying to get the User so I can generate Audit information for any DB write-operations.

A few things I'm doing that can help on the diagnosis:
I am using only app.UseOAuthBearerTokens as authentication with Asp.Net Identity 2. This means that I removed the app.UseCookieAuthentication(new CookieAuthenticationOptions()) that comes enabled by default when you are creating a new Web Api 2.1 project with Asp.Net Identity 2.

Inside WebApiConfig I'm injecting my repository:

builder.RegisterType<ValueRepository>().As<IValueRepository>().InstancePerRequest();

Here's my controller:

[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
    private IValueRepository valueRepository;

    public ValuesController(IValueRepository repo)
    {
        valueRepository = repo;
        // I would need the User information here to pass it to my repository
        // something like this:
        valueRepository.SetUser(User);
    }

    protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        // User is not avaliable here either...
    }
}

But if I inspect the User object on the constructor, this is what I get:

The authentication is working, if I don't pass my token, it will respond with Unauthorized. If I pass the token and I try to access the user from any of the methods, it is authenticated and populated correctly. It just doesn't show up on the constructor when it is called.

In my WebApiConfig I am using:

public static void Register(HttpConfiguration config)
{
    config.SuppressDefaultHostAuthentication();
    config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

    // Web API routes
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

     // ... other unrelated injections using AutoFac
 } 

I noticed that if I remove this line: config.SuppressDefaultHostAuthentication() the User is populated on the constructor.

Is this expected? How can I get the authenticated user on the constructor?

EDIT: As Rikard suggested I tried to get the user in the Initialize method, but it is still not available, giving me the same thing described in the image.


回答1:


The problem lies indeed with config.SuppressDefaultHostAuthentication().

This article by Brock Allen nicely explains why that is. The method sets the principal intentionally to null so that default authentication like cookies do not work. Instead, the Web API Authentication filter then takes care of the authentication part.

Removing this configuration when you do not have cookie authentication could be an option.

A neat solution as mentioned here, is to scope the Web API parts of the application, so that you can separate out this configuration to a specific path only:

public void Configuration(IAppBuilder app)
{
    var configuration = WebApiConfiguration.HttpConfiguration;
    app.Map("/api", inner =>
    {
        inner.SuppressDefaultHostAuthentication();
        // ...
        inner.UseWebApi(configuration);
    });
}



回答2:


Don't know if this is still relevant, but I've had exactly the same problems, as you've described above. I've managed to solve it using custom OWIN middleware component.

Some info about my application structure:

  • Using MVC WebApp and WebAPI in same project (probably not the best option, but I have no time to change it, since deadline is approaching ;))
  • Using AutoFac as IoC container
  • Implemented custom ICurrentContext to hold information about currently logged on user (with CookieAuth in MVC and Bearer Token Auth in WebAPI), which is injected where needed (controllers, BAL objects, etc.)
  • Using EntityFramework 6 for Db access
  • Converted ASP.NET Identity to use int keys rather than string (http://www.asp.net/identity/overview/extensibility/change-primary-key-for-users-in-aspnet-identity)

So on to the code. This is my ICurrentContext interface:

public interface ICurrentContext
{
     User CurrentUser { get; set; } // User is my User class which holds some user properties
     int? CurrentUserId { get; }
}

and implementation of it:

public class DefaultCurrentContext : ICurrentContext
{
    public User CurrentUser { get; set; }

    public int? CurrentUserId { get { return User != null ? CurrentUser.Id : (int?)null; } }
}

I've also created an OWIN middleware component:

using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;

namespace MyWebApp.Web.AppCode.MiddlewareOwin
{
    public class WebApiAuthInfoMiddleware : OwinMiddleware
    {
        public WebApiAuthInfoMiddleware(OwinMiddleware next)
            : base(next)
        {
        }

        public override Task Invoke(IOwinContext context)
        {
            var userId = context.Request.User.Identity.GetUserId<int>();
            context.Environment[MyWebApp.Constants.Constant.WebApiCurrentUserId] = userId;
            return Next.Invoke(context);
        }
    }
}

Some information about this component: MyWebApp.Constants.Constant.WebApiCurrentUserId is some string constant (you can use your own) that I've used to avoid typos since its used in more than one place. Basicly what this middleware does, is that it adds current UserId to the OWIN environment dictionary and then Invokes the next action in pipeline.

Then I've created Use* extension statement to include OMC (OWIN Middleware Component) into OWIN pipeline:

using System;
using Owin;

namespace MyWebApp.Web.AppCode.MiddlewareOwin
{
    public static class OwinAppBuilderExtensions
    {
        public static IAppBuilder UseWebApiAuthInfo(this IAppBuilder @this)
        {
            if (@this == null)
            {
                throw new ArgumentNullException("app");
            }

            @this.Use(typeof(WebApiAuthInfoMiddleware));
            return @this;
        }
    }
}

To use this OMC, I've put the Use* statement right after Use* statement for Bearer token inside my Startup.Auth.cs:

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions); // This was here before

// Register AuthInfo to retrieve UserId before executing of Api controllers
app.UseWebApiAuthInfo(); // Use newly created OMC

Now the actual usage of this principle was inside AutoFac's Register method (called on some bootstrap code at the start of web application; in my case this was inside Startup class (Startup.cs), Configuration method) for my ICurrentContext implementation which is:

private static void RegisterCurrentContext(ContainerBuilder builder)
{
    // Register current context
    builder.Register(c =>
    {
        // Try to get User's Id first from Identity of HttpContext.Current
        var appUserId = HttpContext.Current.User.Identity.GetUserId<int>();

        // If appUserId is still zero, try to get it from Owin.Enviroment where WebApiAuthInfo middleware components puts it.
        if (appUserId <= 0)
        {
            object appUserIdObj;
            var env = HttpContext.Current.GetOwinContext().Environment;
            if (env.TryGetValue(MyWebApp.Constants.Constant.WebApiCurrentUserId, out appUserIdObj))
            {
                appUserId = (int)appUserIdObj;
            }
        }

        // WORK: Read user from database based on appUserId and create appUser object.

        return new DefaultCurrentContext
        {
            CurrentUser = appUser,
        };
    }).As<ICurrentContext>().InstancePerLifetimeScope();
}

This method is called where I build AutoFac's container (hence the input parameter of type ContainerBuilder).

This way I got single implementation of CurrentContext, no matter how user was authenticated (via MVC Web Application or Web API). Web API calls in my case were made from some desktop application, but database and most of codebase were the same for MVC App and Web API.

Don't know if its the right way to go, but it has worked for me. Although I am still a little concerned how would this behave thread-wise, since I don't know exactly how using HttpContext.Current inside API calls would behave. I've read somewhere that OWIN Dictionary is used per-request basis, so I think this is safe approach. And I also think that this isn't so neat code, but rather a little nasty hack to read UserId. ;) If there's anything wrong with using this approcah, I'd appreciate any comment regarding it. I've been strugling with this for two weeks now and this is the closest I got of getting UserId in one place (when resolving ICurrentContext from AutoFac through lambda).

NOTE: Wherever there is usage of GetUserId, it can be replaced with original GetUserId (which returns string) implementation. The reason I'm using GetUserId is because I've rewritten ASP.NET to some extent for using ints instead of strings for TKey. I've done this based on following article: http://www.asp.net/identity/overview/extensibility/change-primary-key-for-users-in-aspnet-identity




回答3:


The User property of the controller is not populated until the Initialize method is called which happens after the constructor is invoked, hence thats why the Identity is not yet populated with the authorzied user data.




回答4:


I realized that removing config.SuppressDefaultHostAuthentication() allowed me to get the Identity in the constructor much earlier. However, I wouldnt suggest doing this if you are using Token Authentication.




回答5:


Thread.CurrentPrincipical is available throughout the pipeline, you could skip the User registration below:

valueRepository.SetUser(User);

and access

Thread.CurrentPrincipical

In the repository instead, making the repository context aware. Furthermore, you could add a context layer.




回答6:


If nothing of the above solutions work try this one:

    public ActionResult GetFiles()
    {
        ...
        string domainID = System.Web.HttpContext.Current.Request.LogonUserIdentity.Name;
        ...
    }


来源:https://stackoverflow.com/questions/23785528/user-iprincipal-not-available-on-apicontrollers-constructor-using-web-api-2-1

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!