Sharing data between Actions and application behavior - asp.net-mvc

This is my application's current workflow for user registration.
register form ( GET /register )
submit the form ( POST /register )
after success, redirect route to "/registerSuccess"
in (GET /registerSuccess ) view, I want to show the message like "username has been registered successfully".
Controller Actions:
public ActionResult Register()
{
return View();
}
[HttpPost]
public ActionResult Register(string name, string password, string confirmPassword)
{
TempData["registered_user"] = name;
return RedirectToRoute("RegisterSuccess");
}
public ActionResult RegisterSuccess()
{
return View();
}
RegisterSuccess View
<h2><%: TempData["registered_user"] %> has been registered successfully.</h2>
It is working fine except the username is empty when user refresh the page (GET /registerSuccess).
Is it normal behavior in other applications? or Should I do something about it? :) (It is my pet project and requirements come from me :P )
update: Actually, there is one more step which is registered user required admin's approval. So, I can't let user log in after successful register. I'm currently using cookie-based FormsAuthentication to track logged in user.
I prefer to use PRG pattern rather than showing flash message in the submit form.

that is normal behaviour.. when the user 'refreshes' the page is requested directly not through your Register function so registered_user key is gone by then. When you do the initial registration you should use a session marker (say a cookie) to track a successfully logged in user.
Using cookies tutorial is a good place to start
If you were to use HTTP authentication (say Basic Auth) then the browser submits the username and password (in clear text for basic auth) with every request that sends out a 401 Unauthorized (see HTTP/1.0 Protocol)

Hmm...
Normally you should think of a SSO solution, that holds your User's authentication (token).
then you would just need to display the current authenticated user (as a completed registration normally logs in the user)
other solution would be, to add the user in the url, so that the success page has all parameters.
/registerSuccess/<username>
(and maybe check, that the logged in user is just registered, and is the same)
for your own fun project, i would do the second one. ist just plain simple.

Related

Programmatically redirect unauthorized users using Microsoft.Owin.Security.OpenIdConnect

When I first started developing my MVC app (which requires authorization, to selectively render a SPA, which in turn does it's own authorization via ADAL.js) I simply decorated the HomeController's Index method with [Authorize] like thus,
[Authorize]
public ActionResult Index(bool ok = false)
{
return View();
}
and everything works fine.
However, sometimes I want users to share URLs with each other. My SPA uses hash-based routing to render various states. As long as both users are logged in already, they can share links with each other and everything magically works ok.
But, if the user clicking a link is yet-to-login, we lose the state communicated in the URL.. everything after the hash.. e.g. if user A sends unauthenticated user B this URL: http://mycorp.com/#/orders/85/edit the HomeController's going to notice user B is unauthenticated, and the middleware is going to kick user B over to our Microsoft login page.. and when he redirects back, the critical #/orders/85/edit portion of the URL in is gone.
My solution was to remove the [Authorize] attribute from the Index action, and instead do something like this:
public ActionResult Index(bool ok=false)
{
if (!User.Identity.IsAuthenticated)
{
if (!ok)
return View("SetCookie_Then_RedirectWith_OK_InQueryString");
if (ok)
//how do I do this part?
return new RedirectResult(Microsoft.Owin.Security.OpenIdConnect.Url);
}
else
//now we're authenticated,
//let JS resume by looking for cookie set previously
return View("Index");
}
Notice the how do I do this part comment above. That's my question.
+++++++++ FWIW ++++++++++
My app features a Startup.Auth.cs file with the familiar middleware declared...
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
...blah...
}
);
I presume I can do something fancy here in Startup.Auth.cs w/Notification Handlers but, I was hoping to directly call the middleware into action from the Index method.

Prevent access to page based on Authentication

I have an ASP.Net MVC 4 Website. When I started this site I had little to no web programming experience, and I don't believe I set up all of it appropriately.
I have pages, such as Home, Login, Register, which I consider public,
pages like, VerifyPIN and AccountCreated, which I consider internal
and pages like Dashboard and Profile, which are private.
I feel public pages should be accessed by anonymous users, but Login and Register should not be accessible once a user logs in.
Internal pages I want to only be available upon server redirect and never through the browser, aka I don't want a user to be able to type in www.MySite.com/AccountCreated.
And private pages are for a logged in user only.
I feel I have private pages working fine, but I don't know how to limit access to the other pages as described above. Perhaps my notion to not allow a logged in user to access the log in page is miss-found.
My site is relatively small due to the ajax nature of the site. I use [AllowAnonymous] on all public pages but then a logged in user can still access those and I am not sure how to prevent that.
My questions are,
How can I prevent a user from accessing a page via the address bar (www.MySite.com/AccountCreated)
How can I prevent access to a [AllowAnonymous] page once a user has logged in.
EDIT
Is it normal to prevent logged on users from accessing certain anonymous pages, such as login?
You may always check if user is already logged in. If he did, he will be redirected to another page:
public ActionResult AccountCreated(some params)
{
if (Request.IsAuthenticated)
{
return RedirectToAction("Index")
}
else
{
// login logic here
}
}
You may also check it directly in View:
#if (Request.IsAuthenticated)
{
<span> You are already logged in</span>
}
else
{
//Login form here
}
well for your first question you can use the [Authorize] action filter.
For your other question, I guess you will have to write your own implementation. You will need to create a new IAuthorizationFilter and use it instead of [AllowAnonymous]
Just some ideas(didn't try them actually).
For question 1 - if AccountCreated is an action that means that the registration form actually POSTs to that URL - i.e. It is accessible from outside. I suggest you apply an HttpPost attribute to it so it will process only POST requests - there is not much you can do here
For point two you can do something like this in your action method
if (HttpContext.User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Home");
}
else return View();
Just a thought, Signout user when a user goes to Login or Register page .
Hope you have not provided any link on private pages for a user to go to login or Register
page or check if user is authenticated redirect it to Home page, frankly you can not stop a user to write a URL and pressing the enter button, or just give a notification that You'll be logged out if you proceed to whatever page.

How do you define the login page you want unauthorized user to be redirected to

I have decorated my controller with an Authorize attribute, as so:
[Authorize(Roles="ExecAdmin")]
If I try to go to that controller after logging in as a user who is not ExecAdmin, it does appear to be attempting to redirect to a login page. BUT, the page it is attempting to redirect to is not my login page, it is a view called LogOnUserControl.ascx. This is a partial view that is not displayed by my login page.
I have no idea why it is doing this -- or maybe it is trying to redirect to some other page altogether, one which does display LogOnUserControl.ascx. Or maybe it is looking for anything with "LogOn" in the name? (Though the name of my login view is LogOn.aspx...)
How can I tell it what page to redirect to?
UPDATE: I do have this in the global.asax
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null || authCookie.Value == "")
{
return;
}
FormsAuthenticationTicket authTicket = null;
try
{
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
}
catch
{
return;
}
string[] roles = authTicket.UserData.Split(new char[] { ';' });
//Context.ClearError();
if (Context.User != null)
{
Context.User = new System.Security.Principal.GenericPrincipal(Context.User.Identity, roles);
}
}
... since I am using a non-standard way of defining roles; i.e., I am not using ASP.NET membership scheme (with role providers defined in web.config, etc.). Instead I am setting roles this way:
// get user's role
string role = rc.rolesRepository.GetUserType(rc.loginRepository.GetUserID(userName)).ToString();
// create encryption cookie
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
1,
userName,
DateTime.Now,
DateTime.Now.AddMinutes(120),
createPersistentCookie,
role //user's role
);
// add cookie to response stream
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
System.Web.HttpCookie authCookie = new System.Web.HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
System.Web.HttpContext.Current.Response.Cookies.Add(authCookie);
(This is called after the user has been validated.)
Not sure how this could be impacting the whole thing, though ...
UPDATE: Thanks to Robert's solution, here's how I solved it -- extend AuthorizeAttribute class:
public class AuthorizeAttributeWithMessage : AuthorizeAttribute
{
private string _message = "";
public string Message
{
get {
return _message;
}
set {
_message = value;
}
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
// user is logged in but wrong role or user:
filterContext.Controller.TempData.Add("Message", Message);
}
base.HandleUnauthorizedRequest(filterContext);
}
}
Then in the LogOn view:
<%
if (HttpContext.Current.Request.IsAuthenticated)
{
// authenticated users should not be here
Response.Redirect("/Home/Index");
}
%>
And in the home page view:
<% if (TempData != null && TempData.Count > 0 && TempData.ContainsKey("Message"))
{ %>
<div class="largewarningtext"><%= TempData["Message"]%></div>
<% } %>
And atop the affected controllers:
[AuthorizeAttributeWithMessage(Roles = "Consultant,ExecAdmin", Message = "You do not have access to the requested page")]
This has the advantage of ALWAYS redirecting any authenticated user who ends up on Logon.aspx -- authenticated users should not be there. If there is a message in the TempData, it will print it out on the home page; if not, it will at least have done the redirect.
Login page is configured within web.config file.
But you probably already know that. The real problem here is a bit more complicated. I guess you're onto something very interesting here, since Login page barely authenticates a user. It doesn't check its authorization for a particular resource (which is your case here where authorization fails) so this shouldn't redirect to login page in the first place.
Checking AuthorizeAttribute source code, you should get a 401: Unauthorize Request response from the server. It doesn't redirect you to the login page (as I anticipated in the previous paragraph, since login is too stupid for that. So there most be something else in your code that doesn't work as it should.
Edit
As this page states:
If the site is configured to use ASP.NET forms authentication, the 401 status code causes the browser to redirect the user to the login page.
Based on this information it's actually forms authentication that sees this 401 and redirects to login (configured as you described in the comment).
But. It would be nice to present some message to the user why they were redirected to login page in the first place. No built-in functionality for that... Still this knowledge doesn't solve your problem, does it...
Edit 2
There are two patterns you can take that actually look very similar to the user, but work diferently on the server.
Simpler one
Write your own authorization attribute (simply inherit from the existing one and add an additional public property Message to it), where you can also provide some sort of a message with attribute declaration like ie.
[AuthorizeWithMessage(Role = "ExecAdmin", Message = "You need at least ExecAdmin permissions to access requested resource."]
Your authorization attribute should populate TempData dictionary with the provided message (check documentation about TempData that I would use in this case) and then call into base class functionality.
change your login view to check for the message in the TempData dictionary. If there is one, you can easily present it to the already authenticated user (along with a link to some homepage that they can access), so they will know why they are presented with a login.
Complex one
create your own authorization filter (not inheriting from the original) and provide your own redirection to some authorization login view that would serve the login in case user has insufficient rights.
create your custom login view that can in this case be strong type. Your authorization filter could populate it with the correct model. This model will include the message string and it can also provide the route link to a page where a user can go.
custom configuration classes that serve this configuration of custom login page.
You could as well configure various different route definitions based on user rights. So for some rights they'd be presented with some page, but if they have some other rights, their route would point to a different route.
Which one to choose?
Go with the simpler one if it satisfies your needs, but if you want more control of the whole process I'd rather go with the complex one. It's not so complicated and it would give you full control of the insufficient login process. You could make it a much better experience for the users.

asp.net mvc: TempData and AuthorizeAttribute

As a followup to this question, I'm wondering what's happening to my TempData.
Scenario 1:
user logs in
user provides email address
user receives email with validation code
user clicks on validation url
user is validated
success msg is displayed via TempData set in Validate action
Scenario 2:
user logs in
user provides email address
user logs out/times out
user receives email with validation code
user clicks on validation url
user is validated
success msg is not displayed via TempData set in Validate action
Now, I don't see a reason for the user to be logged in to validate. In Scenario 1, I put a "Success" message in TempData, and return RedirectToAction("Index"). Index action has an AuthorizeAttribute - if they're not logged in, they're redirected to the login screen (seperate controller).
I would like the login screen to display my message, but TempData appears to get cleared in this scenario. Am I misunderstanding the TempData lifecycle? Does it only apply to requests within the same controller?
The problem is that the AuthorizeAttribute is introducing another redirect into the cycle if the user is not logged in. You are redirecting the user to another action then, if the user is not logged in, the AuthorizeAttribute redirects them to the login page. TempData only lives over one request cycle, so the extra redirect (request) is emptying it and it isn't available on the login page.
You might consider just putting it in the Session directly instead of the TempData front-end to the Session. It should still be there as long as the Session lives.
[Authorize] introduces an extra redirect, which clears the TempData (Tvanfosson has explained the details). So for this to work, you can use a flag on the method you redirect to, for example
return RedirectToAction("Confirm", new { status = "Success!" });
(given that you have the following route and action method declared:)
routes.MapRoute("Confirmation",
"Account/Confirm/{status}",
new { controller = "Account", action = "Confirm", status = "" });
public ActionResult Confirm(string status)
{
return View(status);
}

How to create a view that is not accessible directly but can only be redirected to?

I'm currently working on the user registration in my project. After the registration is done I wish to show some confirmation to the user. I decided to create another view. That's fine.
Now, if after the registration I just return the view like:
public class MyController : Controller
{
[AcceptVerbs (HttpVerbs.Post), ValidateAntiForgeryToken]
public ActionResult Registration (FormCollection form)
{
/* Some logic goes here */
return View ("ConfirmationView");
}
}
Everything is working as desired. No changed url in the title bar. But... If I click the refresh button, the browser will submit the data from the form again which I do not want.
Then I decided to create a separate action, but that means it will produce a new url in the address bar. I do not want the user to click refresh now because this view will not be able to sensibly display the confirmation information again. Is there any way to make an action not accessible directly? Or at least any way to determine whether it was called directly or by redirection? In the latter case I would just take the user away from that page to maybe the home page.
Any way to accomplish this?
So I found the solution myself.
One can use TempData to detect the repeated or external action calls.
public class MyController : Controller
{
[AcceptVerbs (HttpVerbs.Post), ValidateAntiForgeryToken]
public ActionResult Registration (FormCollection form)
{
/* Some logic goes here */
TempData["RedirectCall"] = true;
return RedirectToAction ("Confirmation");
}
[AcceptVerbs (HttpVerbs.Get)]
public ActionResult Confirmation ()
{
if (TempData["RedirectCall"] == null)
return RedirectToAction ("StartPage", "Home");
return View ();
}
}
Nice and simple. :)
One way to solve your problem is to attach a guid or similar type of "random" data to a user session, and check for a valid session when the page is requested. If there is none, you redirect to a page saying that this url is not available at the moment, and that the user will soon be redirected (and then redirect to home after 5 seconds or so using js).
Roughly it would work like this:
When the user is registered, a session cookie is created with for example a GUID. The GUID is also stored in a database table, in which you have one column for the UserID primary key and one for the GUID. You also create an authentication cookie, thus logging the user on to your site.
When all datacalls etc are done, the user has been successfully registered and so on, you redirect to the confirmation page.
When the confirmation page is loaded, the user is automatically logged on (because you created the authentication cookie in step 1). You can then check for a row in the UserID-GUID table corresponding to the logged on user.
a) If there is such a row, you delete the row, and display the confirmation page with all the information.
b) If there is no such row, you display the error message and redirect. As you deleted the row when you showed the message the first time, the user will not be able to access the confirmation page again.
Note: If you use this approach (or some other that makes the confirmation page available only once) you should make sure that it is clearly stated on the confirmation page that the user won't be able to access that page again.
if(Request.UrlReferrer == null)
{
return RedirectToAction("Index");
}

Resources