I am using HangFire to schedule jobs but when I deployed to PROD, the website/hangfire url is not working. I am getting The system cannot find the file specified error.
On localhost, I am able to open the URL.
I followed this URL: http://docs.hangfire.io/en/latest/quick-start.html
Anyone know what I am missing.
Thanks
Hangfire Dashboard exposes sensitive information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is really important to restrict access to the Dashboard.
To make it secure by default, only local requests are allowed, however you can change this by passing your own implementations of the IAuthorizationFilter interface, whose Authorize method is used to allow or prohibit a request. The first step is to provide your own implementation.
http://docs.hangfire.io/en/latest/configuration/using-dashboard.html#configuring-authorization
As Hangfire dashboard exposes sensitive information about your job which includes method names and serialized arguments. Also user can perform different actions like retry, trigger, delete etc. So it is very important to authenticate access to Dashboard.
By default Hangfire allows access to Dashboard pages only for local requests. In order to give appropriate rights for production or testing or UAT users, add your own implementation of authorization using the IDashboardAuthorizationFilter interface for the hangfire dashboard.
http://docs.hangfire.io/en/latest/configuration/configuring-authorization.html
See my sample code below
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
private readonly string[] _roles;
public HangfireAuthorizationFilter(params string[] roles)
{
_roles = roles;
}
public bool Authorize(DashboardContext context)
{
var httpContext = ((AspNetCoreDashboardContext)context).HttpContext;
//Your authorization logic goes here.
return true; //I'am returning true for simplicity
}
}
Asp.net core startup class changes in Configure(IApplicationBuilder app, IHostingEnvironment env) method
Configure(IApplicationBuilder app, IHostingEnvironment env){
......
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
DashboardTitle = "Sample Jobs",
Authorization = new[]
{
new HangfireAuthorizationFilter("admin")
}
});
......
}
Maybe a late answer but might be usefull.
In my case, I had this code :
public class Startup
{
public void Configuration(IAppBuilder app)
{
#if !DEBUG
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthFilter() }
});
#endif
}
}
}
It wasn't working on prod.
I realized that when I copy the application dll from the bin folder, it takes the debug configuration and doesn't start the hangfire.
When I publish the application via visual studio and than copy the DLL from the bin folder of published folder, it works correctly.
Related
I am wanting to create a correlation-id to help analyze logs, but I am wanting to generate a single correlation-id per user "session". I.e. one single correlation id from the start to the end of the application (regardless of the operations performed on the web mvc). I was reading up on how to do this using a middleware in .net. I attempted to implement this in my project, however when I start the application and perform certain operations (homescreen -> 2nd page view -> 3rd page view -> final page view) it will create a new correlation-id for each view. Is there a way to generate one single correlation-id that will be for all the operations performed (home view, 2nd page view, 3rd page view, and final page view)?
Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<CorrelationIdMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true) // allow any origin
.AllowCredentials()); // allow credentials
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
CorrelationIdContext.cs:
public class CorrelationIdContext
{
private static readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>();
public static void SetCorrelationId(string correlationId)
{
if (string.IsNullOrWhiteSpace(correlationId))
{
throw new ArgumentException("Correlation Id cannot be null or empty", nameof(correlationId));
}
if (!string.IsNullOrWhiteSpace(_correlationId.Value))
{
throw new InvalidOperationException("Correlation Id is already set for the context");
}
_correlationId.Value = correlationId;
}
public static string GetCorrelationId()
{
return _correlationId.Value;
}
}
CorrelationIdMiddleware.cs:
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
context.Request.Headers.TryGetValue("correlation-id", out var correlationIds);
var correlationId = correlationIds.FirstOrDefault() ?? Guid.NewGuid().ToString();
CorrelationIdContext.SetCorrelationId(correlationId);
// Serilog
using (LogContext.PushProperty("correlation-id", correlationId))
{
await _next.Invoke(context);
}
}
}
and in my controllers I just have a simple logger i.e._logger.Log(LogLevel.Information, "First page...");
I noticed when debugging in the CorrelationIdMiddleware.cs file, when it hits the line:
var correlationId = correlationIds.FirstOrDefault() ?? Guid.NewGuid().ToString(); it will create a new correlationID even though one already exists. Is there a way to generate one single correlationId for an entire user session (start to end of application)?
context.Request.Headers.TryGetValue("correlation-id", out var correlationIds); value after the first correlationId is created:
CorrelationId as presented here and by many middleware implementations is a logging concept traditionally used to track or correlate multiple actions that spawned off of a single user action, which would normally be represented as a single HTTP Request.
What you haven't adequately described is the lifetime of your expected correlationId. You have specified that the server needs to create it when it does not exist, but in what scenario does it stop existing?
When you want to correlate across multiple requests from the client, then you need to consider one of the following patterns:
Client Side manages the lifetime of the token and passes it through as a HTTP Header or otherwise part of the payload that is sent to the server. Server side simply includes this token in the relevant logs.
This passive server implementation works great for applications that have a back-end distributed over multiple processes, either in the form of web garden, web farm or scaled out cloud infrastructure.
Authentication pattern - Client makes a call to obtain the Id or a token that can be used as a unique identifier for the current user session. As with option 1, the client then makes sure that the token is included as a header with all requests to the server.
This is identical to token based authenticate patterns, in terms of how the client and the server need to interact
You are already using an Authorization middleware, there is a high chance that you could simply use the token used in that process as the correlation Id. If you do not want to store the authentication token, then you could modify your authentication process (and response) to simply create the new token and pass it back either in the payload or as a Http Header, then modify the client to receive this header and pass it back with all requests, at the same time that the authentication token is passed back.
This still works if you want to create a Log Out process or Get New Id, I'm assuming here that the token will remain for the duration of all user interactions for this session.
Server side session context - When processing requests on the server, use session storage to save and retrieve the current correlationId, however I would strongly advise you refer to this as the SessionId or SessionToken.
Refer to Session and state management in ASP.NET Core for guidance on how to setup Session State in the first place.
Session State and the management of it can get complicated if your web deployment is scaled out, it will work well enough in single instance deployments, but to be production ready you need to ensure that your session is not broken when the web app is deployed over multiple servers.
Depending on your chosen implementation, Load balancing may affect your management of the correlation, make sure you set appropriate Session based affinity settings in your network configuration.
Session storage is technically designed for storing this sort of metadata, but you may find it simpler to implement either of the first two options, especially for stateless APIs. Depending on your current environment, enabling Session State may introduce additional complexities that might not be worth the effort if you are using Session Storage for a single keyed value.
I have this ASP.NET Web API and I want to restrict access so it only works when called by specific host. I cannot, for what I know until now, secure it by token, because the WEB API url will be a postback url for a system that will call it automatically when a certain action is made. I have checked out CORS, but what CORS seems to do is to allow a specific domain to access the API. So, does this mean that my WEB API is already restricted for other domains? Then why I can access it by Postman locally, even if it is hosted in Azure?
I just want my service to allow calls from localhost and another specific domain only.
How do I achieve this?
Thanks!
One possibility is to use a custom authorization filter that creates an HTTP response with a failure status code like 400 bad request or 404 not found if the requests has a host that is not allowed. We could define an authorization filter named RestrictDomain that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class RestrictDomainAttribute : Attribute, IAuthorizationFilter
{
public IEnumerable<string> AllowedHosts { get; }
public RestrictDomainAttribute(params string[] allowedHosts) => AllowedHosts = allowedHosts;
public void OnAuthorization(AuthorizationFilterContext context)
{
// Get host from the request and check if it's in the enumeration of allowed hosts
string host = context.HttpContext.Request.Host.Host;
if (!AllowedHosts.Contains(host, StringComparer.OrdinalIgnoreCase))
{
// Request came from an authorized host, return bad request
context.Result = new BadRequestObjectResult("Host is not allowed");
}
}
}
If you want to apply the RestrictDomain filter globally, then you can add the filter in the Startup.cs file like this:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
// Add the restrict domain filter globally
// You could read the allowed hosts from a config file, here we hard code them
options.Filters.Add(new RestrictDomainAttribute("localhost", "example.com"));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
With this setting, if I remove "localhost" from the constructor and only allow "example.com", I get a 400 request when I use Postman since the host will be localhost.
Another option is to use the filter in a controller or controller action directly since we configure it to work as an attribute. However, the allowed hosts will have to be constant values instead of values that can be calculated at runtime. Here's an example:
using Microsoft.AspNetCore.Mvc;
[Route("")]
[ApiController]
public class HomeController : ControllerBase
{
[HttpGet]
public ContentResult Index() => Content("Home");
[HttpGet("greeting")]
[RestrictDomain("localhost", "example.com")] // values must be constants
public ContentResult Greeting() => Content("Hello, World!");
}
If you don't want constant values and don't want to apply the filter globally, then you could inject the IConfiguration into the filter to read the allowed hosts that can access the resource.
CORS on its own only tells the browser / client apps to stop sending requests, in many ASP.Net Web API implementation it doesn't actually block the request pipeline. This is why Postman works, Postman doesn't execute the same pre-flight OPTIONS check first, it just send the request.
You should absolutely look into adding authentication to your API, Bearer token based authentication works well enough with and between APIs. Have a read over Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2, but that is out of scope for this question.
At a conceptual level, you just need to intercept the incoming request in the OWIN pipeline before the API middleware and request the request if it doesn't match your rules. This should be using in conjunction with CORS so that browsers respond in a standard way.
For background, have a read over Block or limit unwanted traffic to Asp.Net Web Application. Usually we manage domain or IP level security filtering in the hosting architecture or routing. IIS or Azure hosts have many built in policies to help manage this.
You could implement this at a global level by adding the following OWIN request processor:
public void ConfigureOAuth(IAppBuilder app)
{
app.Use((context, next) =>
{
string[] AllowedDomains = new string[] { "::1", "localhost", "mybusinessdomain.com" };
if (!AllowedDomains.Contains(context.Request.Host.Value.ToLower()))
{
context.Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
return System.Threading.Tasks.Task.FromResult<object>(null);
}
return next();
});
// TODO: add in your other OWIN configuration AFTER the request filter.
}
If you are NOT using the OWIN hosting pipeline and you want to manage this globally then you could put a check into the global.asax.cs toi handle the Application_BeginRequest:
private static readonly string[] AllowedDomains = new string[] { "::1", "localhost", "mybusinessdomain.com" };
protected void Application_BeginRequest(Object sender, EventArgs e)
{
if >(!AllowedDomains.Contains(HttpContext.Current.Request.UserHostName.ToLower()))
{
HttpContext.Current.Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
HttpContext.Current.Response.End();
// or transfer to a specific page
// Server.Transfer("~/banned.aspx");
}
}
I've been trying to create a SPA web application using React with ASP .NET Core 3.1 as backend, and now I need to restrict users going to certain pages. I know that for API methods I can do the following:
[Authorize(Roles="admin")]
[HttpGet]
public async Task<Whatever> Get(){ ... }
But that would only block users from using the API methods, which is good, but I also want to not let them go into the pages themselves.
Since I do not want to use their Blazor pages (because it breaks the separation of the client app and the backend), I cannot scaffold their login page, so I created mine using React and then implemented login and logout methods, which work, since when I'm logged in, the AspNetCore.Identity.Application cookie appears.
In order to do this, I added the following lines to Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentityCore<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
services.AddAuthentication (o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
}).AddIdentityCookies(o => {});
...
}
public void Configure(IApplicationBuilder app)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Now I've read (source: https://stackoverflow.com/a/40055744/14806778) that, in React, you can define an onEnter method and check authentication there. So, to check if user is logged in or not, I implemented the simple method:
public bool IsUserLoggedIn()
{
return User.Identity.IsAuthenticated;
}
This also works, so I guess I could call this method in the onEnter method of React routing. I don't know if that's efficient or not, though.
Last, to check if user is in role, I could do something like this:
public async Task<bool> IsUserInRole(string requiredRole)
{
if(User.Identity.IsAuthenticated)
{
var user = await _userManager.GetUserAsync(User);
return await _userManager.IsInRoleAsync(user, requiredRole);
}
return false;
}
And call this onEnter instead.
My question is, is this approach valid? Is it safe? Does it have a huge performance impact? What are the alternatives? I've looked around a bit but I haven't seen a lot. I've read about JWT but I don't know how is it so different to this.
Also, I don't need Google/Apple/Facebook login, this is for an app which will be most likely running on localhost inside a VPN. I am using .NET Core 3.1, React 16.14 and React router dom 5.2.0.
Thank you.
A website is per default anonymous access only.
The admin has a button to switch the site into maintenance mode, which should enable authorization using the built-in CookieAuthentication (flip a bit in a database, not relevant for this post).
In order to make that work, I first configured cookie authentication (in startup.cs):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
Then on relevant controllers, I put an [Authorize] attribute.
[Authorize]
public class HomeController : Controller
{
//removed
}
This works perfectly - cookie auth kicks in when authorize-attribute is present. So far so good.
Now I want to disable authorization at runtime when maintenance mode is off.
Attempted Solution
This is what I ended up with after a lot of trial and error and research.
public void OnAuthorization(AuthorizationFilterContext context)
{
IMaintenanceModeDataService ds = context.HttpContext.RequestServices.GetService<IMaintenanceModeDataService>();
if (!ds.IsMaintenanceModeEnabled)
{
//Maintenance mode is off, no need for authorization
return;
}
else
{
ClaimsPrincipal user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
//when the user is authenticated, we don't need to do anything else.
return;
}
else
{
//we're in maintenance mode AND the user is not
//It is outside the scope of this to redirect to a login
//We just want to display maintenancemode.html
context.Result = new RedirectResult("/maintenancemode.html");
return;
}
}
}
[MaintenanceModeAwareAuthorize]
public class HomeController : Controller
{
//removed
}
This works great when the site is in maintenance mode.
When the site is NOT in maintenance mode, the cookie authentication still kicks in and requires auth. I could remove that and try to implement my own auth, but that would be stupid, when we already have perfectly well-crafted solutions built-in.
How do I disable authorization when the site is NOT in maintenance mode (at runtime)?
Notes:
Q: Why not handle this by doing x (which requires serverside access to config, environment vars, server or similar)?
A: Because this needs to be immediately accessible to non-technical admin-users by clicking a button in the backend.
Yes you can!
The authorization system in ASP.NET Core is extensible and you can implement your scenario easily with poliy-based authorization.
Two main things to know to get going:
an authorization policy is made of one or more requirements
all of the requirements must be satisfied for a policy to succeed
Our goal is then to create a requirement which is satisfied if any of the following statements is true:
the maintenance mode is not enabled, or
the user is authenticated
Let's see the code!
The first step is to create our requirement:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirement : IAuthorizationRequirement
{
}
We then have to implement the handler for this requirement, which will determine if it's satisfied or not. The good news is handlers support dependency injection:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler : AuthorizationHandler<MaintenanceModeDisabledOrAuthenticatedUserRequirement>
{
private readonly IMaintenanceModeDataService _maintenanceModeService;
public MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler(IMaintenanceModeDataService maintenanceModeService)
{
_maintenanceModeService = maintenanceModeService;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MaintenanceModeDisabledOrAuthenticatedUserRequirement requirement)
{
if (!_maintenanceModeService.IsMaintenanceModeEnabled || context.User.Identities.Any(x => x.IsAuthenticated))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Next, we need to create an authorization policy that uses this requirement, and you have 2 choices here:
you can redefine the default policy, used when "empty" [Authorize] attributes are used, or
create an explicit policy that you'll have to reference in your attributes, like [Authorize(Policy = "<your-policy-name>")]
There's no right or wrong answer; I'd pick the first option is my application had only one authorization policy, and the second one if it had several of them. We'll see how to do both:
services
.AddAuthorization(options =>
{
// 1. This is how you redefine the default policy
// By default, it requires the user to be authenticated
//
// See https://github.com/dotnet/aspnetcore/blob/30eec7d2ae99ad86cfd9fca8759bac0214de7b12/src/Security/Authorization/Core/src/AuthorizationOptions.cs#L22-L28
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement())
.Build();
// 2. Define a specific, named policy that you can reference from your [Authorize] attributes
options.AddPolicy("MaintenanceModeDisabledOrAuthenticatedUser", builder => builder
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement()));
});
Next, you need to register the requirement handler as an IAuthorizationHandler, as indicated in the official docs
// The lifetime you pick is up to you
// You just need to remember that it's got a dependency on IMaintenanceModeDataService, so if you
// registered the implementation of IMaintenanceModeDataService as a scoped service, you shouldn't
// register the handler as a singleton
// See this captive dependency article from Mark Seeman: https://blog.ploeh.dk/2014/06/02/captive-dependency/
services.AddScoped<IAuthorizationHandler, MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler>();
The final step is to apply the [Authorize] attributes on your controllers/actions as needed.
// 1. If you redefined the default policy
[Authorize]
public IActionResult Index()
{
return View();
}
// 2. If you defined an explicit policy
[Authorize(Policy = "MaintenanceModeDisabledOrAuthenticatedUser")]
public IActionResult Index()
{
return View();
}
I am afraid that could not be done .The accept of authorization is different from authentication, when context.HttpContext.User.Identity.IsAuthenticated is false, it will always redirect to login page.
It's better to have actions that must or may require authorization in a controller together, and unauthorized actions in a separate controller with [AllowAnonymous].
if (!user.IsMaintenanceModeEnabled)
{
context.Result = new RedirectResult("Another controller with [AllowAnonymous]");
return;
}
Since current pages need work perfectly with anonymous mode, then authentication should NOT be in Controller level.
I think your requests are:
If a Maintancer login system,
run extra code to show maintance elements(switch button or others) on page, so Maintancer can switch page with different mode, and do maintancer actions
If user visit site anonymously, anonymous-mode elements will render to browser
If user login but not an Maintancer, normal-user-mode elements will render to browser
To resolve those, The key is to block unauthorized user to visit Maintancer ACTIONS, instead of controller.
my suggestions are:
in _Layout.cshtml page, check if Maintancer Login, then enject switch button
in the actions or pages that could visit anornymously, check if "Maintancer Login" && IsMaintenanceMode, then show Maintancer-authorized elements, like Delete Post, Edit Content, ...
in Controller.Actions that works only for Maintancer(like Delete Post), add [Authorize(Roles="Maintancer")] or [Authorize(Policy="Maintancer")] or you customized authorize.
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.
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);
});
}
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
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.
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.
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.
If nothing of the above solutions work try this one:
public ActionResult GetFiles()
{
...
string domainID = System.Web.HttpContext.Current.Request.LogonUserIdentity.Name;
...
}