ASP.NET 5 MVC application method sets HttpContext.Response cookie.
How to read this cookie value in other method called from contoller via long call chain in same request ?
Such method does not exist in response collection interface
public interface IResponseCookies
{
void Append(string key, string value);
void Append(string key, string value, CookieOptions options);
void Delete(string key);
void Delete(string key, CookieOptions options);
}
Current request TempData values set in other methods can read. Why cookies cannot ? Should cookie settings duplicated in HttpContext.Items or is there better method ?
Background:
Shopping cart application has log method called from controllers.
It must log cartid
If user adds product first time to cart, controller creates cart id using new guid and adds cartid cookie to response.
logger method uses Request.Cookies["cartid"] to log cart it.
For first item added to cart it return null since cookie is not set to browser.
Response.Cookies["cartid"]
does not exist.
Log method can called from many places. It is difficult to pass cartid as parameter to it.
Application has log method called from controllers. It logs controller context to same database used by controllers.
Logging is performed in Error controller created using ASP.NET Core application template:
public async Task<IActionResult> Error()
{
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
await logger.LogExceptionPage(exceptionHandlerPathFeature);
HttpContext.Response.StatusCode = 500;
return new ContentResult() {
Content ="error"
};
}
How to log Response cookie in this method et by code executed before error ?
Code which causes compile error:
public class CartController : ControllerBase
{
const string cartid = "cartid";
private readonly HttpContextAccessor ca;
public CartController(HttpContextAccessor ca)
{
this.ca = ca;
}
public IActionResult AddToCartTest(int quantity, string product)
{
ca.HttpContext.Response.Cookies.Append(cartid, Guid.NewGuid().ToString());
Log("AddToCartStarted");
return View();
}
void Log(string activity)
{
Console.WriteLine($"{activity} in cart {ca.HttpContext.Response.Cookies[cartid]}");
}
}
You cannot read the Cookies from Response. You need to read it from the Request as follow
ca.HttpContext.Request.Cookies[cartId];
cartId is the key for the value stored in Cookie.
Related
I am using RedirectToAction method of the base API controller. The first controller needs to modify the request headers (which I seem to be able to do) but the change is not retained.
This is a simplified implementation of what I am trying to do.
[HttpGet]
public IActionResult Get(string str)
{
// The request comes with a request header with key "Authorization" and value "ABC"
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", "XYZ");
return RedirectToAction("B");
}
[HttpGet]
public IActionResult B()
{
var value = HttpContext.Request.Headers.First(x => x.Key == "Authorization"); // I want this to be ""XYZ" , but it remains "ABC"
return Ok();
}
Any ideas on how I can get the second Action to use the Request header I updated in the first Action.
Edit:
All our Controllers / Actions are Authenticated using Core middleware JWT Auth Policy
Only Action one allows Anonymous access.
Action one is called from an internal related solution with a "Code" (Which is used to create a JWT token) .
All other Actions require the JWT for Authentication.
The "Code" and JWT Token have independent expires and also hold some data.
The info in the "Code" is deemed more up to date. Action one can be called with both a "Code" and a JWT token.
The redirect was my way of flushing out old JWT token.
When action one is called with a valid "Code" a new JWT is created. I then want to redirect to a controller that will validate the JWT token (using middleware).
I know I can do this manually, but trying to trigger core middleware.
RedirectToAction returns a 302 response to the browser, which tells the browser to start a new request to the new URL. So your code in B() is hit after the browser initiated a whole new request. So the Request object you're looking at in B() is completely different than the one you modified in Get().
That's clearly not what you want to do. Instead of redirecting, just return B():
[HttpGet]
public IActionResult Get(string str)
{
// The request comes with a request header with key "Authorization" and value "ABC"
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", "XYZ");
return B();
}
But something does feel wrong about rewriting the request headers. It's kind of like rewriting history. Another way is to use HttpContext.Items to store a value:
[HttpGet]
public IActionResult Get(string str)
{
HttpContext.Items["Authorization"] = "XYZ";
return B();
}
[HttpGet]
public IActionResult B()
{
var value = HttpContext.Items["Authorization"] as string ?? HttpContext.Request.Headers.First(x => x.Key == "Authorization");
return Ok();
}
The values in HttpContext.Items exist for the life of a single request.
If you must redirect, then you can use TempData to hold data until it's read in the next request:
[HttpGet]
public IActionResult Get(string str)
{
TempData["Authorization"] = "XYZ";
return RedirectToAction("B");
}
[HttpGet]
public IActionResult B()
{
if (TempData.Contains("Authorization")) {
HttpContext.Request.Headers.Remove("Authorization");
HttpContext.Request.Headers.Add("Authorization", TempData["Authorization"]);
}
var value = HttpContext.Request.Headers.First(x => x.Key == "Authorization");
return Ok();
}
In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.
The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.
The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
// todo: update the Client Cookie
await _signInManager.RefreshSignInAsync(User); // wrong type
}
How do I refresh the cookie?
public static class HttpContextExtensions
{
public static async Task RefreshLoginAsync(this HttpContext context)
{
if (context.User == null)
return;
// The example uses base class, IdentityUser, yours may be called
// ApplicationUser if you have added any extra fields to the model
var userManager = context.RequestServices
.GetRequiredService<UserManager<IdentityUser>>();
var signInManager = context.RequestServices
.GetRequiredService<SignInManager<IdentityUser>>();
IdentityUser user = await userManager.GetUserAsync(context.User);
if(signInManager.IsSignedIn(context.User))
{
await signInManager.RefreshSignInAsync(user);
}
}
}
Then use it in your controller
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
await HttpContext.RefreshLoginAsync();
}
Or abstract it in an action filter
public class RefreshLoginAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await context.HttpContext.RefreshLoginAsync();
await next();
}
}
Then use it like this in your controller
[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
// your normal controller code
}
This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).
Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.
We use the following flow (js client app connecting to .NET Core API):
User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
The client receives the token, userID, and refreshToken and stores them in localStorage.
When the original token expires (1 hour), the client gets a 401 error from the API.
The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
The client the repeats the call that originally errored in a 401 with the new token.
Here is the code we added to the User Service:
public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
try
{
var result = await _cognitoIdentityProvider.InitiateAuthAsync(
new InitiateAuthRequest
{
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
ClientId = _pool.ClientID,
AuthParameters = new Dictionary<string, string>
{
{ "REFRESH_TOKEN", refreshToken },
{ "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
}
});
if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
return new UserLoginResult(UserLoginStatus.Failed);
return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
}
catch
{
return new UserLoginResult(UserLoginStatus.Failed);
}
}
private static string HmacSHA256(string data, string key)
{
using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
{
var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(result);
}
}
IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.
AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.
UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.
Please note that setting SECRET_HASH may not be required based on your config is Cognito.
I have two controllers Base and Login.
Base Controller:
public ActionResult start()
{
string action = Request.QueryString[WSFederationConstants.Parameters.Action];
}
Login Controller:
public ActionResult Login(string user,string password,string returnUrl)
{
if (FormsAuthentication.Authenticate(user, password))
{
if (string.IsNullOrEmpty(returnUrl) && Request.UrlReferrer != null)
returnUrl = Server.UrlEncode(Request.UrlReferrer.PathAndQuery);
return RedirectToAction("Start","Base", returnUrl });
}
return View();
}
After authentication is done it gets redirected to Start action in Base Controller as expected.
But the querystring doesnot fetch the value. When hovered over the querystring it shows length value but not the uri.
How to use the url sent from Login controller in Base Controller and fetch parameters from it?
You are actually returning a 302 to the client.
From the docs.
Returns an HTTP 302 response to the browser, which causes the browser
to make a GET request to the specified action.
When doing that the client will make another request with the url that you created. In your case something like youruri.org/Base/Start. Take a look at the network tab in your browser (F12 in Chrome).
What I think you want to do is:
return RedirectToAction
("Start", "Base", new { WSFederationConstants.Parameters.Action = returnUrl });
Assuming that WSFederationConstants.Parameters.Action is a constant. If WSFederationConstants.Parameters.Action returns the string fooUrl your action will return the following to the browser:
Location:/Base/Start?fooUrl=url
Status Code:302 Found
Another option is to actually pass the value to the controller:
public class BaseController: Controller
{
public ActionResult start(string myAction)
{
string localAction = myAction; //myAction is automatically populated.
}
}
And in your redirect:
return RedirectToAction
("Start", "Base", new { myAction = returnUrl });
Then the BaseController will automatically fetch the parameter, and you don't need to fetch it from the querystring.
So my project requirements changed and now I think I need to build my own action filter.
So, this is my current login controller:
public class LoginController : Controller
{
// GET: Login
public ActionResult Index()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model)
{
string userName = AuthenticateUser(model.UserName, model.Password);
if (!(String.IsNullOrEmpty(userName)))
{
Session["UserName"] = userName;
return View("~/Views/Home/Default.cshtml");
}
else
{
ModelState.AddModelError("", "Invalid Login");
return View("~/Views/Home/Login.cshtml");
}
}
public string AuthenticateUser(string username, string password)
{
if(password.Equals("123")
return "Super"
else
return null;
}
public ActionResult LogOff()
{
Session["UserName"] = null;
//AuthenticationManager.SignOut();
return View("~/Views/Home/Login.cshtml");
}
}
And this is my action filter attempt:
public class AuthorizationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (HttpContext.Current.Session["UserName"] != null)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary{{ "controller", "MainPage" },
{ "action", "Default" }
});
}
base.OnActionExecuting(filterContext);
}
}
I have already added it to FilterConfig, but when I login it does not load Default.cshtml it just keeps looping the action filter. The action result for it looks like this:
//this is located in the MainPage controller
[AuthorizationFilter]
public ActionResult Default()
{
return View("~/Views/Home/Default.cshtml");
}
So, what would I need to add in order to give authorization so only authenticated users can view the application´s pages? Should I use Session variables or is there another/better way of doing this using? I am pretty much stuck with AuthenticateUser(), since what happens there now is just a simple comparison like the one we have there now.
Thank you for your time.
Create an AuthorizeAttribute with your logic in there:
public class AuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
{
// Don't check for authorization as AllowAnonymous filter is applied to the action or controller
return;
}
// Check for authorization
if (HttpContext.Current.Session["UserName"] == null)
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
As long as you have the Login URL Configured in your Startup.Auth.cs file, it will handle the redirection to the login page for you. If you create a new MVC project it configures this for you:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(
new CookieAuthenticationOptions {
// YOUR LOGIN PATH
LoginPath = new PathString("/Account/Login")
}
);
}
}
Using this you can decorate your controllers with [AuthorizationFilter] and also [AllowAnonymous] attributes if you want to prevent the authorization from being checked for certain Controllers or Actions.
You might want to check this in different scenarios to ensure it provides tight enough security. ASP.NET MVC provides mechanisms that you can use out of the box for protecting your applications, I'd recommend using those if possible in any situation. I remember someone saying to me, if you're trying to do authentication/security for yourself, you're probably doing it wrong.
Since your attribute is added to the FilterConfig, it will apply to ALL actions. So when you navigate to your MainPage/Default action it will be applying the filter and redirecting you to your MainPage/Default action (and so on...).
You will either need to:
remove it from the FilterConfig and apply it to the appropriate actions / controllers
or add an extra check in the filter so that it doesn't redirect on certain routes
I am trying to create another layer between my controller and my view so that I can pass different versions of a view to a user based on their "client ID" which would be the company to which they belong.
I have the following code:
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
// set client
var client = new Client();
client.Id = Guid.NewGuid();
client.Name = "Foo";
// set user
var user = new User();
user.Id = Guid.NewGuid();
user.ClientId = client.Id;
user.Name = "Foo";
return ViewRenderer.RenderView("AddComplete", client);
}
}
My ViewRenderer class looks like this:
public static class ViewRenderer
{
public static ViewResult RenderView(string view, Guid clientId)
{
string viewName = GetViewForClient(view, clientId);
return Controller.View(view);
}
public static string GetViewForClient(string view, Guid clientId)
{
// todo: logic to return view specific to the company to which a user belongs...
}
}
The problem is, the line return Controller.View(view); in RenderView(string view, Guid clientId) gives me the error:
System.Web.Mvc.Controller.View()' is inaccessible due to its
protection level
I am interested to know how I can resolve this error or if there is a better way to do what I am trying to do, which is to display different versions of a view which are specific to the respective company to which a user belongs.
Edit: Another option I was kicking around in my head...
Is there a way to override the View() method such that I can prepend it with a directory name, for example, a user who belongs to "Acme Co." would call the same controller action as everyone else like View("MyView") but the method would actually be calling View("AcmeCo/MyView") however, I don't actually write that code in my controller, it's just derived from the user's client ID property.
You can just replace the view engine instead of adding another abstraction.
Write your own View engine (here is how to start off with a RazorViewEngine)
public class ByIdRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
var id = // get something from controller context controllerContext
var newViewPath = CalculateViewPathFromId(id);
return base.CreateView(controllerContext, newViewPath, masterPath);
}
And register it in Global.asax.cs:
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ByIdRazorViewEngine());
}
The View() method is a protected member. You can only access it from within a derived type, such as your HomeController class. Plus you're trying to access it as a static method.
You can create a base Controller that exposes your specialized view logic. For the sake of illustration, I'm going to call it DynamicViewControllerBase
public class HomeController : DynamicViewControllerBase
{
//
// GET: /Home/
public ActionResult Index()
{
// set client
var client = new Client();
client.Id = Guid.NewGuid();
client.Name = "Foo";
// set user
var user = new User();
user.Id = Guid.NewGuid();
user.ClientId = client.Id;
user.Name = "Foo";
return RenderView("AddComplete", client);
}
}
public class DynamicViewControllerBase : Controller
{
protected ViewResult RenderView(string view, Guid clientId)
{
string viewName = GetViewForClient(view, clientId);
return View(view);
}
// Unless you plan to use methods and properties within
// the instance of `Controller`, you can leave this as
// a static method.
private static string GetViewForClient(string view, Guid clientId)
{
// todo: logic to return view...
}
}
If all you want to have is the company name prefixed to your controllers, apply the RoutePrefix attribute on to your controller.
Example:
[RoutePrefix(#"{company}")]
public partial class HomeController : Controller
{
}
And in your RouteConfig file,
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Make sure this line is added
routes.MapMvcAttributeRoutes();
}
Since your users must be authenticated to sign in to their accounts, once they've authenticated them selves you can either:
Store a cookie on your users machine with the name of their company
Make calls to your database on each request to retrieve this information
Make use of ViewData[]
etc..
Once you have the name of their company, you can construct the urls with that name.
Example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel model)
{
// ... authenticate user etc
// Redirect to
// foo.com/abc/home
return this.RedirectToAction("Index", "Home", new { company = "abc" });
}
If you're trying to work a way around this, I doubt you'll be able to as the web request first comes through a route, and the route decides which controller/action is executed, but to know the company name your action needs to execute to retrieve.