Because of the requirements of my project, I'm wanting to provide custom authenticate for my MVC controller actions. Therefore, I will not be using SetAuthCookie().
Intially I set a cookie as follows;
string userData = EncDec.MakeString(user.Email + "|" + user.UserId);
//the Cookie and FormsAuthenticationTicket expiration date/time is the same
DateTime cookieExpiry = DateTime.Now.AddMinutes(AccountPage.MvcApplication.COOKIE_EXPIRY_MINUTES);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // ticket version
user.UserName, // authenticated username
DateTime.Now, // issueDate
cookieExpiry, // expiryDate
false, // true to persist across browser sessions
userData, // can be used to store additional user data
FormsAuthentication.FormsCookiePath); // the path for the cookie
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
//create the cookie
HttpCookie cookie = new HttpCookie("ADV_" + Extensions.ControllerExtensionMethods.GetGuid(this), encryptedTicket);
cookie.Secure = true;
cookie.HttpOnly = true;
cookie.Expires = cookieExpiry;
Response.Cookies.Add(cookie);
The HttpCookie is being saved in the client browser with a encrypted FormsAuthenticationTicket.
Then within my controller actions, whenever I need to check and verify that the user is authenticated I call this method;
public static FormsAuthenticationTicket IsAuthenticated(string guid)
{
HttpCookie cookie = HttpContext.Current.Request.Cookies["ADV_" + guid];
if (cookie != null)
{
string encryptedTicket = cookie.Value;
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encryptedTicket);
if (!ticket.Expired)
{
//if the user is authenticated and the cookie hasn't expired we increase the expiry of the cookie - keep alive
DateTime cookieExpiry = DateTime.Now.AddMinutes(AccountPage.MvcApplication.COOKIE_EXPIRY_MINUTES);
//create a new ticket based on the existing one
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(
ticket.Version, // ticket version
ticket.Name, // authenticated username
ticket.IssueDate, // issueDate
cookieExpiry, // expiryDate, changed to keep alive if user is navigating around site
false, // true to persist across browser sessions
ticket.UserData, // can be used to store additional user data
ticket.CookiePath); // the path for the cookie
string newEncryptedTicket = FormsAuthentication.Encrypt(newTicket);
//keep alive
HttpCookie newCookie = new HttpCookie("ADV_" + guid, newEncryptedTicket);
newCookie.Secure = true;
newCookie.HttpOnly = true;
newCookie.Expires = cookieExpiry;
HttpContext.Current.Response.Cookies.Set(newCookie);
return newTicket;
}
}
return null;
}
Every time the user is re-authenticated, I am increasing the time out of when the cookie will expire, so that the login is keep alive.
Everything seems to work fine, and the users are correctly authenticated, and if they aren't authenticated I redirect them to a login page, and they can't access methods if they aren't authenticated either.
My questions are:
Is this way of dealing with the authentication secure.
Is there anything I should be aware of, in terms of a security risk.
Thanks.
You basically need to look at creating a Custom Authentication Attribute.
I'm not going to provide the actual implementation here, but this will put you on the right path.
Here's the basic representation :-
public class GoogleAuthAttribute : FilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
IIdentity ident = filterContext.Principal.Identity;
if (!ident.IsAuthenticated || !ident.Name.EndsWith("#google.com"))
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
{
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "GoogleAccount"},
{"action", "Login"},
{"returnUrl", filterContext.HttpContext.Request.RawUrl}
});
}
}
}
I've took this from the Apress Book i'm currently reading on MVC5. If OnAuthentification fails, you set the Result property of the AuthenticationContext, this is then passed to the AuthenticationChallengeContext, where you can add your challenge code.
In my example, the user is redirected to the login page.
All you need t do, is place this AuthentificationAttribute on the Action Methods you require.
You should be able to build in or work your custome security code in to this.
You should really ask yourself if its a good idea to be adding custom security measures, as it can lead to more problems that you want.
Related
So here's a break down of my issue in steps:
The user logs in with Google.
On the login callback, information about the user is gathered
Roles are assigned based on the user
A FormsAuthenticationTicket is created which passes the user/roles to the Application_PostAuthenticateRequest in Global.asax
In that request a GenericPrinciple is created from the authentication ticket and roles
HttpContext.Current.User is set to the above variable
Now my question is, now that I set who the current user is that is using the website, how can I reference them? After the post authenticate complete, I check the current user and it's null. Should I be setting the principle to a different variable other than HttpContext.Current.User?
Callback
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
...
// Get roles for current user
string roles = "bob,adminbob,cthulu";
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
loginInfo.Email,
DateTime.Now,
DateTime.Now.AddMinutes(30), // value of time out property
false,
roles,
FormsAuthentication.FormsCookiePath);
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
Response.Cookies.Add(authCookie);
...
}
Post Authenticate in Global.asax
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e) {
HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null) {
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
string[] userRoles = authTicket.UserData.Split(new Char[] { ',' });
GenericPrincipal userPrincipal = new GenericPrincipal(new GenericIdentity(authTicket.Name), userRoles);
HttpContext.Current.User = userPrincipal; //How do I reference this in the program?
}
}
Nevermind I figured out why it was returning blank. When I was calling the method to check the Name, the Identity wasn't authenticated yet. If you try to get the name without it being authenticated, it returns blank.
So I just simply checked with:
HttpContext.Current.User.Identity.IsAuthenticated
On a login form I have an option to allow the user to click a remember me checkbox which creates a new FormsAuthenticationTicket which then gets added to a cookie.
if (_model.RememberMe)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
_model.Username,
DateTime.Now,
DateTime.Now.AddDays(30),
true,
_model.Username,
FormsAuthentication.FormsCookiePath);
// Encrypt the ticket.
string encTicket = FormsAuthentication.Encrypt(ticket);
// Create the cookie.
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
Which should hopefully be in the clients browser for 30 days as stated above.
Testing this, I've purposely left the current session timeout for only a minute
<sessionState timeout="1"></sessionState>
So after a minute, if the user has said "remember me" I expect the website should not be redirected back to the login page. However it does. This is the code that does it.
// [".ASPXAUTH"] is the cookie name that is created by the FormsAuthenticationTicket`
if (User.Identity.Name == "" && Request.Cookies[".ASPXAUTH"] == null)
{
return RedirectToAction("LogOut", "Login");
}
// the current session hasn't timed out or the remember me cookie is enabled
FormsIdentity id = (FormsIdentity)User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
But the cookie is NULL.
I am expecting it's a misunderstanding on my behalf so if anyone can give me a hand. I would be very grateful.
Thanks
What you are looking for is
string mySessionCookie = System.Web.HttpContext.Current.Request.Headers["Cookie"];
if (mySessionCookie.IndexOf(".ASPXAUTH", StringComparison.Ordinal) >= 0) {
// do something
}
EDIT
How about this, I haven't tested it but I remember doing something like this before
HttpCookie cookie = (HttpCookie)(Request.Cookies[FormsAuthentication.FormsCookieName]);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
My scenario is probably the opposite of most, I want to ALLOW multiple simultaneous logins but only for different types of users.
User — Has their own area
Admin — Has their own area
The problem occurs as an Admin can be a User as well (they have two accounts, this is mainly so they can check how the system is working from a user PoV) and want to be logged into both at the same time.
With Forms authentication, this doesn't seem possible. So I've had to "hack" around it slightly and am worried I might have overlooked something.
Plan:
Two action filters for each type of user: UserAuthorise &
AdminAuthorise
Have two session cookies for each type of user
Decorate controllers which the correct action filter based on what
user can access it.
Code might need some tidying up.
I'll make the cookie names more unique as well.
Excluded stuff like Views/Routes as they don't seem relevant.
Left password salting/hashing out of samples and stuck with test values.
UserAuthorise:
public class UserAuthorize : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var authCookie = filterContext.RequestContext.HttpContext.Request.Cookies["User"];
if (authCookie == null || authCookie.Value == "")
{
filterContext.HttpContext.Response.Redirect("/login");
base.OnActionExecuting(filterContext);
return;
}
FormsAuthenticationTicket authTicket;
try
{
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
}
catch
{
filterContext.HttpContext.Response.Redirect("/login");
base.OnActionExecuting(filterContext);
return;
}
if (authTicket.Expired || authTicket.Expiration <= DateTime.Now)
{
filterContext.HttpContext.Response.Redirect("/login");
}
base.OnActionExecuting(filterContext);
}
}
AdminAuthorise:
public class AdminAuthorise : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var authCookie = filterContext.RequestContext.HttpContext.Request.Cookies["Admin"];
if (authCookie == null || authCookie.Value == "")
{
filterContext.HttpContext.Response.Redirect("/admin/login");
base.OnActionExecuting(filterContext);
return;
}
FormsAuthenticationTicket authTicket;
try
{
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
}
catch
{
filterContext.HttpContext.Response.Redirect("/admin/login");
base.OnActionExecuting(filterContext);
return;
}
if (authTicket.Expired || authTicket.Expiration <= DateTime.Now)
{
filterContext.HttpContext.Response.Redirect("/admin/login");
}
base.OnActionExecuting(filterContext);
}
}
User Login controller action:
[HttpPost]
public virtual ActionResult Login(FormCollection form)
{
if (form["username"] == "admin" && form["password"] == "pass")
{
var authTicket = new FormsAuthenticationTicket(
1, // version
form["username"], // user name
DateTime.Now, // created
DateTime.Now.AddMinutes(20), // expires
false, // persistent?
"" // can be used to store roles
);
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
var authCookie = new HttpCookie("User", encryptedTicket);
Response.Cookies.Add(authCookie);
// Redirect back to the page you were trying to access
return RedirectToAction(MVC.Home.Index());
}
else
{
ModelState.AddModelError("", "Bad info mate");
}
return View();
}
Admin Login controller action:
[HttpPost]
public virtual ActionResult Login(FormCollection form)
{
if (form["username"] == "admin" && form["password"] == "pass")
{
var authTicket = new FormsAuthenticationTicket(
1, // version
form["username"], // user name
DateTime.Now, // created
DateTime.Now.AddMinutes(20), // expires
false, // persistent?
"" // can be used to store roles
);
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
var authCookie = new HttpCookie("Admin", encryptedTicket);
Response.Cookies.Add(authCookie);
// Redirect back to the page you were trying to access
return RedirectToAction(MVC.Admin.Home.Index());
}
else
{
ModelState.AddModelError("", "Bad info mate");
}
return View();
}
Does this all seem sensible and secure?
Looking in FireFox's Page Info window at cookies I see each user type has its own cookie and you can't access a user type area without logging in.
First, you should probably be deriving from AuthorizeAttribute rather than ActionFilterAttribute. AutorizationFilters exectue before ActionFilters, and allow short-circuiting (that is, if an authorization filter fails, action filters will never execute). Also, ActionFilters are chained together, and might execute in any order.
Second, it's not a good idea to have the admin username and password hard coded into the attribute. Passwords should really be one-way hashed.
What you need for this scenario is called impersonation, basically all you have to do is set a fake authentication cookie with the impersonated user data (so the admin can see what the customer see).
Probably you would also want to keep track of this, so you can place on the user interface of the admin impersonating the user infos about the state of the application, and also give it a link to end the impersonation session (you would at this point restore the previous cookie), instead of letting him "log in twice".
You can check this out, as it may contain some useful infos for you (a bit old but always valid).
Regarding your database model, I'd assign several roles (simple user, admin, supervisor, etc) to a user. This way, you would login just once using a default role (admin), and have an option to switch to another role (simple user PoV) and store permissions on session.
I have this application that uses custom methods to register and loggin users using FormsAuthentication. The server where this is hosted has a policy of restarting the sessions every 15 minutes and when that happens all my users get logged out. The code to loggin a user is:
var user = this.accountRepo.GetUser(id);
// Create the forms authentication cookie
var cookieValue = user.name;
HttpCookie cookie = FormsAuthentication.GetAuthCookie(cookieValue, true);
// Dercrypt the cookie
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
// Create a new ticket with the desired data
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket
(
ticket.Version,
ticket.Name,
ticket.IssueDate,
DateTime.Now.AddYears(1),
true,
user.Authentication
);
// Update the cookies value
cookie.Value = FormsAuthentication.Encrypt(newTicket);
Response.Cookies.Set(cookie);
accountRepo.Login(user);
With the Forms cookie created and with my Authentication data, which is basically the users hashed password, I then use the following logic to display the Login button or the username:
#{
var accountRepo = new AccountRepository();
var user = accountRepo.GetCurrentUser();
}
#if(user != null && user.LoggedIn) {
<div>#Html.ActionLink(Context.User.Identity.Name + " - Logout", "LogOff", "Account", null, new { #class = "logout_link" })</div>
}
else
{
#Html.ActionLink("Login", "Login", "Account", new { returnUrl = Request.Url.AbsoluteUri }, new { #class = "login_link" })
}
And that "GetCurrentUser()" method is:
var cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
return db.Users.SingleOrDefault(u => u.Authentications.Equals(ticket.UserData, StringComparison.CurrentCultureIgnoreCase));
}
return null;
Am I missing something here? I believe that with this code It should matter if the session restarts, my users should stay logged in.
Thanks in advance.
It's just as Mystere Man said. The cookie name was getting re-generated every time the session rebooted, so the app was looking for the cookie with a different name than what it had before.
For the peace of mind of all of you that helped me, and for the developer that will support this app in the future, I refactored it so its not that "evil" anymore :P
I am working with MVC 3 and I have just implemented a wrapper for the FormsAuthenticationService.
Something similar to the following.
public void SignIn(string username, bool createPersistantCookie)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentException("Value Cannot be null or empty", "username");
FormsAuthentication.SetAuthCookie(username, createPersistantCookie);
}
Reluctantly, I have gotten this to work, but now I am not quite sure how to get the information that I have stored.
Once the user is in my system, how can I now safely retrieve this information if I need to grab their UserID out of the database?
Based on the additional information provided, you want to store additional data with the FormsAuthentication ticket. To do so, you need first create a custom FormsAuthentication ticket:
Storing Data
Grab the current HttpContext (not worrying about testability)
var httpContext = HttpContext.Current;
Determine when the ticket should expire:
var expires = isPersistent
? DateTime.Now.Add(FormsAuthentication.Timeout)
: NoPersistenceExpiryDate; // NoPersistenceExpiryDate = DateTime.MinValue
Create a new FormsAuthentication ticket to hold your custom data.
var authenticationTicket = new FormsAuthenticationTicket(
1,
username,
DateTime.Now,
DateTime.Now.Add(FormsAuthentication.Timeout),
isPersistent,
"My Custom Data String"); //Limit to about 1200 bytes max
Create your HTTP cookie
new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authenticationTicket))
{
Path = FormsAuthentication.FormsCookiePath,
Domain = FormsAuthentication.CookieDomain,
Secure = FormsAuthentication.RequireSSL,
Expires = expires,
HttpOnly = true
};
And finally add to the response
httpContext.Response.Cookies.Add(cookie);
Retrieving Data
Then you can retrieve your data on subsequent requests by parsing the stored authentication ticket...
Again, grab current HttpContext
var httpContext = HttpContext.Current
Check to see if the request has been authenticated (call in Application_AuthenticateRequest or OnAuthorize)
if (!httpContext.Request.IsAuthenticated)
return false;
Check to see if you have a FormsAuthentication ticket available and that it has not expired:
var formsCookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (formsCookie == null)
return false;
Retrieve the FormsAuthentication ticket:
var authenticationTicket = FormsAuthentication.Decrypt(formsCookie.Value);
if (authenticationTicket.Expired)
return false;
And finally retrieve your data:
var data = authenticationTicket.UserData;
You haven't actually stored a user id in the database. All the code that you've written does is store an authentication cookie on the users computer, either as a session cookie (not persistent) or as a persistent one.
When your page refreshes, it will get the cookie automatically, decode it, and populate the IPrincipal object which you access from the User.Current property of your controller.