I am trying out the Finbuckle multi tenant library now and have managed to set the tenant information for the user.
However, when I redirect to another page and load the tenantinfo in the view, it shows up as null. Am I doing something wrong?
This is how I get and set my tenantinfo:
var OrganizationId = HttpContext.User.Claims.Where(xc => xc.Type == MultiTenancy.ClassLibrary.Globals.CookieGlobals.OrganizationId).Select(xc => xc.Value).SingleOrDefault();
var tenantInfo = await store.TryGetByIdentifierAsync(OrganizationId);
HttpContext.TrySetTenantInfo(tenantInfo, resetServiceProvider: true);
var ti = HttpContext.GetMultiTenantContext()?.TenantInfo;
return RedirectToAction("Sample", "Home", new { __tenant__ = tenantInfo.Name });
This is the Sample View:
#using Microsoft.AspNetCore.Identity
#{
ViewData["Title"] = "Sample";
Layout = "~/Views/Shared/_Layout.cshtml";
var tenantInfo = Context.GetMultiTenantContext()?.TenantInfo;
}
#using Finbuckle.MultiTenant.AspNetCore
<h2>#tenantInfo.Name</h2>
EDIT:
So far what works is if I do this:
public async Task<ActionResult> Sample()
{
var OrganizationId = HttpContext.User.Claims.Where(xc => xc.Type == MultiTenancy.ClassLibrary.Globals.CookieGlobals.OrganizationId).Select(xc => xc.Value).SingleOrDefault();
var tenantInfo = await store.TryGetByIdentifierAsync(OrganizationId);
HttpContext.TrySetTenantInfo(tenantInfo, resetServiceProvider: true);
var ti = HttpContext.GetMultiTenantContext()?.TenantInfo;
return View(ti);
}
But that means that the tenantinfo has to always be set. Is there a way to make it persistent?
This is my Startup.cs:
services.AddMultiTenant()
.WithEFCoreStore<MultiTenancyDbContext, TenantInfo>()
.WithDelegateStrategy(context => {
var organizationId = ((HttpContext)context).User.Claims.Where(xc => xc.Type == MultiTenancy.ClassLibrary.Globals.CookieGlobals.OrganizationId).Select(xc => xc.Value).SingleOrDefault();
return Task.FromResult(organizationId);
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
);
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
});
Thanks for pinging me over on the GitHub site for Finbuckle.MultiTenant.
The reason the tenant information is lost is because the Redirect to Action redirects the user browser so its like an entirely new HTTP request which basically resets everything. The second approach you posts doesn't actually redirect the user browser so it doesn't lose the info. Finbuckle.MultiTenant should work with either approach--I'll try to clarify how it works below.
It looks like you are setting the tenant info yourself, but while this is possible it is not the intended flow. The library was designed so that its middleware would use a strategy, query the store, and set the tenant info before your controller code is ever hit. It would look something like this (where WithStrategy and WithStore are simplified for this example):
// In your services configuration in your Startup class:
services.AddMultiTenant().WithStrategy().WithStore();
// In your app pipeline in your Startup class:
app.UseMultiTenant(); // <-- This sets the tenant info
So in normal situations you don't need to call store.TryGetByIdentifierAsync(...) or HttpContext.SetTenantInfo(...).
Instead you want to define a strategy that gets the organization ID from the cookie like you did. The middleware will use the strategy at the start of every request to set the tenant info. There is a DelegateStrategy that should work for your situation. It requires you to set a delegate or lambda that will return the tenant identifier, in your case the organization id:
services.AddMultiTenant().WithStore().
WithDelegateStrategy(context =>
{
var organizationId = HttpContext.User.Claims.Where(xc => xc.Type == MultiTenancy.ClassLibrary.Globals.CookieGlobals.OrganizationId).Select(xc => xc.Value).SingleOrDefault();
return Task.FromResult(organizationId);
});
Please check out the delegate strategy sample in the repository to see a working project that uses a similar approach.
Related
In an ASP.NET Core 3.1 web app, I can change the target route for unauthorised requests in my Startup.cs like so:
services.ConfigureApplicationCookie(o =>
{
o.AccessDeniedPath = "/Home/Error";
});
That will return /Home/Error?ReturnUrl=... where ... is whatever page I was trying to access.
But I actually just want it to return simply "/Home/Error?code=401"
I tried e.g.
o.AccessDeniedPath = "/Home/Error?code=401"
but that brings back simply
"/Home/Error?code=401?ReturnUrl=%2FAdmin"
I then realised there's a ReturnUrlParameter in the options, like this:
o.ReturnUrlParameter = "code";
o.AccessDeniedPath = "/Home/Error";
which gets me this far in the redirect:
/Home/Error?code=%2FAdmin
But I want to specify the keyvalue value (e.g. 401), i.e. replace the page the request came from, so the final result would be
/Home/Error?code=401
According to your code, it seems that you are using cookie Authentication, I suggest yo could try to change the redirect URL using the CookieAuthenticationEvents.OnRedirectToAccessDenied Property, check the following sample code:
services.AddAuthentication("CookieAuthentication")
.AddCookie("CookieAuthentication", config =>
{
config.Cookie.Name = "UserLoginCookie"; // Name of cookie
config.LoginPath = "/Login/UserLogin"; // Path for the redirect to user login page
config.AccessDeniedPath = "/Login/UserAccessDenied";
config.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents()
{
OnRedirectToAccessDenied = ctx =>
{
var redirectPath = ctx.RedirectUri;
if (redirectPath.Contains("?ReturnUrl"))
{
//remove the ReturnURL
var url = redirectPath.Substring(0, redirectPath.LastIndexOf("?ReturnUrl"));
ctx.Response.Redirect(url + "?code=401");
}
// Or, directly using the following code:
//ctx.Response.Redirect("/Login/UserAccessDenied?code=401");
return Task.CompletedTask;
}
};
});
The output like this:
Hello stack overflow peeps,
Having a bit of an issue implementing an oAuth system.
//https://github.com/justintv/Twitch-API/blob/master/authentication.md
public ActionResult AuthorizeTwitch(CancellationToken cancellationToken) {
var twitchBridge = new TwitchBridge();
var codes = Request.Params.GetValues("code");
var token = Request.Params.GetValues("access_token");
// stage 1 Initial Handshake
if (codes == null && token == null) {
return Redirect(twitchBridge.AuthorizeUrl());
}
// stage 2 We have a code so we are at stage two request the token
else if (codes != null) {
return Redirect(twitchBridge.RetrieveToken(codes[0]));
}
// SAVE OAUTH STUFF DOWN HERE.
// THEN REDIRECT
return RedirectToAction("Index", "Home");
}
The above action seems to work however when for stages 1 and 2 but when I get a response form stage 2 I get redirected to a URL that looks like.
http://localhost:58434/Home/AuthorizeTwitch#access_token=anaccesstoken&scope=user_read
This hits my action but I cant access the value for access_token. I can access Request.Url.OriginalString but the return string looks like.
http://localhost:58434/Home/AuthorizeTwitch
Debugging and looking into the request object and the URL object nothing seems to have stored anything from the # onwards. I suspect this has something to do with the routes setup for my site.
The relevant route that is being hit is
routes.MapRoute(
name: "Default2",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Can any one see what i'm doing wrong?
In case it is something to do with my actually request to twitch this is how im building the url.
public string AuthorizeUrl() {
return string.Format(
"{0}{1}?response_type=code&client_id={2}&redirect_uri={3}&scope={4}",
TwitchUrlBase,
TwitchAuthorizeMethod,
ClientId,
HttpUtility.UrlEncode(TwitchAuthorizeRedirectURL),
TwitchAuthorizeScope
);
}
public string RetrieveToken(string code) {
return string.Format(
"{0}{1}?response_type=token&client_id={2}&client_secret={3}grant_type=authorization_code&redirect_uri={4}&scope={5}",
TwitchUrlBase,
TwitchAuthorizeMethod,
ClientId,
ClientSecret,
HttpUtility.UrlEncode(TwitchAuthorizeRedirectURL),
TwitchAuthorizeScope
);
}
The fragment is not supposed to be sent by the client to the server, so it makes sense that it's not there. Did you mean to send a query?
I have an ASP.NET app. My app has a _ViewStart.cshtml file. That file looks like this:
#using MyCompany.MyApp;
#{
Layout = "/Views/Shared/_Layout.cshtml";
var p = HttpContext.Current.Request.QueryString["parameter"];
ViewBag.QSParameter = p;
}
When I execute this code, I get the following error:
The name 'HttpContext' does not exist in the current context
I don't understand. Isn't _ViewStart.cshtml kind of the "shell" for the views? I'm trying to figure out how to globally read a query string parameter and set a value on the ViewBag for each request. I thought this would be the way to do it.
Thanks
You should have access to Request in your _ViewStart file.
Try this:
#using MyCompany.MyApp;
#{
Layout = "/Views/Shared/_Layout.cshtml";
var p = Request.QueryString["parameter"];
ViewBag.QSParameter = p;
}
EDIT: For ASP.NET 5
I don't have ASP.NET 5 on my machine but have looked at the source code for the framework. It looks like there is a Context property on RazorPage that returns an HttpContext. Alternatively, you can access the HttpContext through the ViewContext. See below:
#{
Layout = "/Views/Shared/_Layout.cshtml";
var p = Context.Request.Query["parameter"];
// or this...
// var p = ViewContext.HttpContext.Request.Query["parameter"];
ViewBag.QSParameter = p;
}
To retrieve it from _ViewStart.cshtml, you can use:
ViewBag.QSParameter = Context.Request.Query["parameter"];
Note: Use Query now (over QueryString) in ASP.NET 5
However, I might ellect to go a different route and take advantage of IResultFilter:
public class QSParameterFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
var QSParameter = context.HttpContext.Request.Query["parameter"];
((Controller)context.Controller).ViewBag.QSParameter = QSParameter;
}
public void OnResultExecuted(ResultExecutedContext context) { }
}
Then, register it within your Startup.cs:
services.AddMvc();
services.Configure<MvcOptions>(options => {
options.Filters.Add(new QSParameterFilter());
});
Make the "Build Action = Content" in the file properties. This will solve the issue.
In an existing C# Web project here at my Job I've added a Web API part.
In four of my own classes that I use for the Web API I need to access some of the existing Controller-classes. Right now I just create a new Instance of them and everything works as intented: ProductController controller = new ProductController();
Still, creating a new ProductController while one should already exist obviously isn't a good practice. I know the Controllers are created in the Config-file in the Routes.MapHttpRoute, since it's using the C# Web MVC method. Below I've copied that piece of code:
config.Routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "MyProject.Controllers" }
);
route.DataTokens["UseNamespaceFallback"] = false;
I've tried to access these Controllers in my one of my API-classes like so:
private void getControllerInstance()
{
var url = "~/Products";
// Original path is stored and will be rewritten in the end
var httpContext = new HttpContextWrapper(HttpContext.Current);
string originalPath = httpContext.Request.Path;
try
{
// Fake a request to the supplied URL into the routing system
httpContext.RewritePath(url);
RouteData urlRouteData = RouteTable.Routes.GetRouteData(httpContext);
// If the route data was not found (e.g url leads to another site) then authorization is denied.
// If you want to have a navigation to a different site, don't use AuthorizationMenu
if (urlRouteData != null)
{
string controllerName = urlRouteData.Values["controller"].ToString();
// Get an instance of the controller that would handle this route
var requestContext = new RequestContext(httpContext, urlRouteData);
var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
// TODO: Fix error (The controller for path '/Products' was not found or does not implement IController.) on this line:
var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);
controller = (ProductController)controllerbase;
}
}
finally
{
// Reset our request path.
httpContext.RewritePath(originalPath);
}
}
As you might have noticed by the TODO-comment, at the line var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);, I get the following error:
HttpException was unhandler by user code: The controller for path '/Products' was not found or does not implement IController.
Does anyone know how to fix this error? Has this got something to do with one of the following two lines of the code in the Config-file?
namespaces: new[] { "MyProject.Controllers" }
route.DataTokens["UseNamespaceFallback"] = false;
Or did I do something else wrong?
A tip to everyone: Don't continue programming when you are very, very tired.. Anyway, everything was correct except for a small flaw:
My API Controller is called ProductsController and my normal (default) controller is called ProductController. In the method above I use:
var url = "~/Products";
To access the ProductController..
So, after removing the "s" (and for good measure make everything lower case) I have the following instead:
var url = "~/product";
And now it works..
I have a MVC4 WebApi project with routing that is working correctly with an optional "id" parameter in the route:
routes.Add(new ApiRouteInfo
{
Name = this.AreaName.ToLower() + "_readingsplans",
RouteTemplate = baseUrl + "/plans/readingalerts/{id}",
Defaults = new
{
area = this.AreaName.ToLower(),
controller = "ReadingAlerts",
id = RouteParameter.Optional
}
});
When making an actual request the routing works to hit either the GetAll or Get method in the controller methods:
public HttpResponseMessage GetAll(BaseQueryFilter filter)
public HttpResponseMessage Get(int id)
But in the unit test, the RouteTester object always hits the Get method, not the GetAll.
Works:
Assert.AreEqual(ReflectionHelper.GetMethodName((ReadingAlertsController p) => p.Get(It.IsAny<int>())), routeTester.GetActionName());
Fails:
Assert.AreEqual(ReflectionHelper.GetMethodName((ReadingAlertsController p) => p.GetAll(null)), routeTester.GetActionName());
I've tried passing in an actual filter object instead of null but that doesn't change the outcome at all.
I know I can fix it by creating two different routes, but I'm a bit reluctant since the current routing does work for everything except the unit test.
Any suggestions?
Did you look at this? It explains a lot about unit testing web api and it may be useful to you.
I found a stackoverflow thread which describes how to test out the route. I am using something similar that I found on the net, but I am willing to try it.
Here is another article with a similar implementation. This is what I am using and having a similar issue with.
--Updated--
I believe I found the fix for the issue. Using the article mentioned above, I replaced the 'GetActionDescriptor()' function with the following:
private HttpActionDescriptor GetActionDescriptor()
{
if (controllerContext.ControllerDescriptor == null)
GetControllerType();
var actionSelector = new ApiControllerActionSelector();
var results = actionSelector.GetActionMapping(controllerContext.ControllerDescriptor);
try
{
return actionSelector.SelectAction(controllerContext);
}
catch
{
var subActions = results[request.RequestUri.Segments.Last()];
var action = subActions.FirstOrDefault(a => a.SupportedHttpMethods.First(m => m.Method == request.Method.Method) != null);
return action;
}
}