I currently working on an extension of the filter [Authorize], so that I can retrieve the permissions from the database. Everything works, but it is sure a performance issue. Every time I send a query to the database, asking for permission, and that is not the best way to determine this. So I thought to put these data in the Session. What is the fastest way to put a data from database in to Session object which I could ask (LINQ) as well as databases.
Now this looks like:
var _allowedRolesDB = context.sec_RolesInCAs
.Where(rl => rl.MenuControlName == controllRights && rl.MenuActionName == actionRights)
.Select(rl => rl.RoleName);
foreach (var r in _allowedRolesDB)
{
RolesDB = RolesDB + r.ToString() + ",";
}
but I want change to
var _allowedRolesDB = MySuperSessionSomethink
.Where(rl => rl.MenuControlName == controllRights && rl.MenuActionName == actionRights)
.Select(rl => rl.RoleName);
foreach (var r in _allowedRolesDB)
{
RolesDB = RolesDB + r.ToString() + ",";
}
where MySuperSessionSomethink will keep one-time-retrieved data from database. Any idea how I can do this? Tx for help.
BIGGER PICTURE
Ok. I will show bigger picture.
Whole idea is to create custom authorize filter.
[CustomAuthAttribute("Home,Index", Roles = "SuperAdministrator")]
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
What is a goal of this. Create authorize attribute which has all benefits, plus additional features like keep information about rights in database.
Now what I do is:
public CustomAuthAttribute(params string[] controllerAction)
{
IPrincipal user = HttpContext.Current.User;
string userName = user.Identity.Name;
**... some code .. and take all allowed roles and check it have permissions**
var _allowedRolesDB = context.sec_RolesInCAs
.Where(rl => rl.MenuControlName == controllRights && rl.MenuActionName == actionRights)
.Select(rl => rl.RoleName);
foreach (var r in _allowedRolesDB)
{
RolesDB = RolesDB + r.ToString() + ",";
}
**... some code .. thesame withs single users**
}
After this i use
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
**... can acces or not part of code ...**
if (_rolesSplit.Any(user.IsInRole))
{
return true;
}
}
But there is the problem. Everytime I ask database about permissions, and this IMHO is not best way. Now my idea is take all permissions for one user and put him into his session when hi is authorized. Maybe I'm wrong and this way will make problems, but keep in database and ask all the time about permissions is not good idea too :). So maybe better way to take data and use in code after only one or two questions to the database?
First of all, it seems like you would want to add these to the cache, and not the session. They seem global to the application, not specific to the user. Since you're doing a lookup of roles by menu control/action, I would just add them to the cache as a lookup:
string action = controlRights + actionRights;
string allowedRoles = Cache[action];
if (allowedRoles == null)
{
allowedRoles = String.Join(",", context.sec_RolesInCAs
.Where(rl => rl.MenuControlName == controlRights && rl.MenuActionName == actionRights)
.Select(rl => rl.RoleName)
.ToArray());
Cache[action] = allowedRoles;
}
This will give you the allowed roles for the given control/action, from the cache on the second request.
Related
I have implemented the IdentityServer4 SSO in my application. SSO works fine as well as Logout for all the clients,However there is a new requirement where if the user is already logged in into an application and if he tries to login again (From different devise/browser) then he should be automatically logged out of the previous browser. I am not getting my head around this.How to implement this and if it is possible at all to track the user login sessions?
Update:-
We have tried following way of doing it, We have added the Session info into the Global Static variables using "Action" filter attribute.Here we stored the Login Session Info after user gets logged in.
private class LoginSession
{
internal string UserId { get; set; }
internal string SessionId { get; set; }
internal string AuthTime { get; set; }
internal DateTimeOffset AuthDateTime
{
get
{
if (!string.IsNullOrEmpty(AuthTime))
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(AuthTime));
else
return DateTimeOffset.UtcNow;
}
}
}
private static List<LoginSession> LoginSessions = new List<LoginSession>();
In "Action Filter" methods we check if the user's session id is already present or not. If the session is present and it's SessionId is not matching with claims session id then we check the Login time of the Session. If the login time is less than the current login time then the user is logged out of the system else we update the login session with the latest session id and login time. Due to this workflow for the second login the Login Session will be updated as the Login Time is always Greater than the saved Login Session Info. And for the old Logged in session the user will be logged out of the system as the login time would always be less than the updated session info.
public class SessionValidationAttribute : ActionFilterAttribute
{
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
string action = context.RouteData.Values["action"].ToString();
if (!string.IsNullOrEmpty(action) &&
context.Controller.GetType().GetMethod(action).GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Length == 0)
{
var claims = ((ClaimsIdentity)((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller).User.Identity).Claims;
var sessionId = claims.Where(x => x.Type == "sid").First().Value; // context.HttpContext.Request.Cookies.TryGetValue("idsrv.session", out var sessionId);
var userId = claims.Where(x => x.Type == "sub").First().Value;
var authTime = claims.Where(x => x.Type == "auth_time").First().Value;
var authDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(authTime));
if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0) // if already logged in
{
var latestLogin = LoginSessions.Where(x => x.UserId == userId).OrderByDescending(x => x.AuthDateTime).First();
if (sessionId != latestLogin.SessionId)
{
if(authDateTime > latestLogin.AuthDateTime) // login using new browser(session)
{
latestLogin.SessionId = sessionId; // assign latest sessionId
latestLogin.AuthTime = authTime; // assign latest authTime
}
else if (authDateTime < latestLogin.AuthDateTime) // login using old browser(session)
{
LoginSessions.RemoveAll(x => x.UserId == userId && x.SessionId!=latestLogin.SessionId);
context.Result = ((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller)
.RedirectToAction(actionName: "Logout", controllerName: "Home",
routeValues: new { tenant = string.Empty, isRemoteError = false });
}
}
}
else
{
var newLogin = new LoginSession() { UserId = userId, SessionId = sessionId, AuthTime = authTime };
LoginSessions.Add(newLogin);
}
}
return base.OnActionExecutionAsync(context, next);
}
}
This works as we tested for few users but Will this solution work in actual scenario where there are thousands of users login into the system?Is it a good idea to use Static variable globally for storing session info? What will be potential drawbacks of using this.Please advice. We are open to new ideas also,if there is any new methods of implementing this functionality please let us know.
Thanks!!!
Disclaimer: I have no practical experience with IS4.
You probably have a good reason but I'm failing to understand why you are overwriting the latestLogin session's details when you are validating the current latest login?
If I'm not mistaken this line will loop through all sessions in your application, which you have multiple alike of in the lines that follow.
if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0)
This is indeed something you wouldn't want to do in an application you expect to scale.
Unfortunately I'm not familiar with IS4 and I can not tell you if there is a possibility to solve this problem entirely by utilizing its APIs, instead I can give you practical advice.
You can use a separate centralized storage, the possibilities are endless but something along the lines of memcached is perfect. Then the algorithm is fairly simple:
Whenever a user tries to log in, retrieve the value stored under user's ID from storage.
If present, that would be the current session ID, then destroy it in IS4 and continue.
Create new login session and store the session ID in memcached under the user's ID.
This way there will never be more than 1 session for a given user and you've successfully reduced the complexity of the algorithm from O(n), or worse in your case, to O(1).
Here is what I'm trying to achieve. Certain options at the navbar should be available only if the user has "subordinates" in the database.
So, at the navbar I have:
The Approvals should be hidden for some users, but available to others. For those whom it should be available, the user must:
A) Be a Supervisor or,
B) Have a subornidate at the DB table
So, as for "A" it's pretty straightforward. I did:
#if (User.IsInRole("Supervisor"))
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
For "B", I was suggested to use Sessions. Well, great. So I came to the question: how can I make a single request to the DB and assign it to a Session["HasSubordinates"] so I can do this check?
#if (User.IsInRole("Supervisor") || (bool)Session["HasSubordinates"])
{
<li>#Html.ActionLink("Approvals", "Index", "Approval")</li>
}
What I tried was to have:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
for every single controller, but that didn't worked well because sometimes I get null pointer and it looks absolutely rubbish.
I know it may sound like a trivial question for some (or most), but I'm really stuck and I do really appreciate any help.
Looking at your code, getting a user subordinates should only happen once. In your Login method:
Session["HasSubordinates"] = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
Create a new class to extend IPrincipal:
public class IPrincipalExtensions
{
public bool HasSubordinates(this IPrincipal user)
{
return Session != null && Session["HasSubordinates"] != null && Session["HasSubordinates"] > 0;
}
}
Now, in the View:
#if (User.IsInRole("Supervisor") || User.HasSubordinates() )
{
}
Writing from memory, may have left something out, but this should be the cleanest.
Don't use the session for this. What you need is a child action.
[ChildActionOnly]
public ActionResult Nav()
{
var model = new NavViewModel
{
IsSupervisor = User.IsInRole("Supervisor");
HasSubordinates = _uow.ApprovalService.GetSubordinates(User.Identity.Name).Count() > 0;
}
return ParialView("_Nav", model);
}
Then, just create a partial view, _Nav.cshtml and utilize the properties on the view model to render your nav however you like.
If you want, you can even use output caching on the child action, so it's only evaluated once per user. There's no built-in way to vary the cache by user, so first, you'll need to override the following method in Global.asax:
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
var args = custom.ToLower().Split(';');
var sb = new StringBuilder();
foreach (var arg in args)
{
switch (arg)
{
case "user":
sb.Append(User.Identity.Name);
break;
case "ajax":
if (context.Request.Headers["X-Requested-With"] != null)
{
// "XMLHttpRequest" will be appended if it's an AJAX request
sb.Append(context.Request.Headers["X-Requested-With"]);
}
break;
default:
continue;
}
}
return sb.ToString();
}
With that, you can then just decorate your child action with:
[OutputCache(Duration = 3600, VaryByCustom = "User")]
I have a simple login form on my mvc application and it works correctly except for when you try to login with a user that isn't in the database.
It will throw an error Sequence contains no elements which makes sense since theres no matching users in the database although I try to handle it in the code it is not doing it.
// POST: Login User
[HttpPost]
public ActionResult Login(UserAccount user)
{
using (MyDbContext db = new MyDbContext())
{
var usr = db.Users.Single(u => u.UserName == user.UserName && u.UserPassword == UserPassword);
if (usr != null)
{
Session["UserID"] = usr.UserId.ToString();
Session["Username"] = usr.UserName.ToString();
return RedirectToAction("LoggedIn");
}
else
{
ModelState.AddModelError("", "Username or Password Incorrect");
}
}
return View();
}
Single will throw an exception if no match is found, or too many are found. Using SingleOrDefault like so returns null in the case where no match is found:
var usr = db.Users.SingleOrDefault(...)
This sets you up for the null check you're doing right afterwards.
So...As some people said in the comments above, you could just simply use the following query and then do your null check:
var usr = db.Users.SingleOrDefault(u => u.UserName == user.UserName && u.UserPassword == UserPassword);
That being said, I would recommend looking into using the built in ASP Identity framework for authentication and authorization - unless you need a custom implementation for your application, you can avoid a lot of testing (and potential bugs) as well as get a ton of cool out-of-the-box features. I would recommend checking out the resources here for more information.
I have the following ActionFilter class, to implement my custom authorization system:-
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CheckUserPermissionsAttribute : ActionFilterAttribute
{
Repository repository = new Repository();
public string Model { get; set; }
public string Action { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string ADusername = filterContext.HttpContext.User.Identity.Name.Substring(filterContext.HttpContext.User.Identity.Name.IndexOf("\\") + 1);
if (!repository.can(ADusername,Model,Action))
{
filterContext.Result = new HttpUnauthorizedResult("You cannot access this page");
}
base.OnActionExecuting(filterContext);
}
}
The above class will call the following repository method:-
public bool can(string user, string Model, string Action)
{
bool result;
bool result2;
int size =tms.PermisionLevels.Where(a5 => a5.Name == Action).SingleOrDefault().PermisionSize;
var securityrole = tms.SecurityroleTypePermisions.Where(a => a.PermisionLevel.PermisionSize >= size && a.TechnologyType.Name == Model).Select(a => a.SecurityRole).Include(w=>w.Groups).Include(w2=>w2.SecurityRoleUsers).ToList();
foreach (var item in securityrole)
{
result = item.SecurityRoleUsers.Any(a => a.UserName.ToLower() == user.ToLower());
var no = item.Groups.Select(a=>a.TMSUserGroups.Where(a2=>a2.UserName.ToLower() == user.ToLower()));
result2 = no.Count() == 1;
if (result || result2) {
return true;
}}
return false;
}
But inside my repository method , I am doing the following:-
Query the database and include all the Groups & SecurityRoleUsers when executing the .tolist()
Then filter the returned records insdie the server, based on the foreach loop.
But this will cause the following drawbacks:-
If I have many Groups and SecurityRoleUsers, then I will be getting them all from the DB, and then filter the result on the server.
And since this code will be executed whenever an action method is called, as it Is a security attribute at the begging of the controller class. So this might not be very efficient.
So my question is whether I can join all the queries inside the repository method to be single query , and do all the work on the Database and just return true or false to the server ?
The associated tables looks as follow:-
Ideally remove this foreach.
Try riding with Linq to Sql.
You should be more comfortable because it resembles SQL.
This link has several examples.
http://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b
Att
Julio Spader
wessolucoes.com.br
Use linq.
Ideally you should only have one line of code after you got the size value. e.g.
int size =tms.PermisionLevels.Where(a5 => a5.Name == Action).SingleOrDefault().PermisionSize;
var result = //One line of code to determine user authenticity
return result;
I think you should design you database in the way that join queries are easy to do. So you don't have to perform more than one select.
Try code-first EF, which links tables very easily.
You need to take care with Lazy Loading. If not used correctly, it will make a query to the database each object segmentation, especially in your foreach. With that already has a good improvement.
Take a look at this article. I think it will help you too.
http://www.sql-server-performance.com/2012/entity-framework-performance-optimization/
Att
Julio Spader
wessolucoes.com.br
I have an MVC3 application and I would like to give the users the ability to set preferences that would be enabled when the user logs in.
I really don't have any idea where to start with this and would really appreciate being pointed in the right direction. I did try some changes to the membership class but now I am thinking that's probably not the best way to go about things.
You could do it in a database (sounds like you might be using one at least with the out-of-the-box membership provider) once uniquely identifying a user. In that case, you may want to implement your own membership provider.
You have to do a little work to start implementing your own provider. If this is your only requirement, you might be able to avoid it by writing your own class that returns settings in a format of your choosing
public static class UserSettings
{
public static string GetSettings(IPrincipal user)
{
if(user.Identity.IsAuthenticated)
{
// dip into database using user.Identity.Name property
return "string with user settings";
// this also assumes user.Identity.Name is uniquely able
// to identify a user in your database!
}
return string.Empty;
}
}
Or, if the information is completely trivial, maybe you could implement a cookie representation of the user settings. This, of course, comes with all the caveats of using cookies, but you could avoid storing the information in a database
Anywhere you have an HttpContext you could grab the settings value like so:
if(HttpContext.Current != null)
{
string userSettings = HttpRequest.Current.Request.Cookies["NameOfCookie"];
}
You can use the FormsAuthentication cookie to store your user information and avoid accessing the database all the time. That cookie is encrypted and whatever information you're storing as safe as the user session itself. The only problem with the cookies is that they have a maximum size of 4K so, if your user info is massive then you might run into a problem. When I use the cookie approach I store my user data as a JSON and then deserialize that JSON on each page request. Here is my login controller logic (I'm using SimpleMembership but the approach is the same:
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, model.RememberMe))
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var user = _userLogic.GetItem(model.UserName);
if (user != null && user.IsActive)
{
var newAuthTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name, authTicket.IssueDate, authTicket.Expiration, authTicket.IsPersistent, JsonConvert.SerializeObject(user));
var newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newAuthTicket))
{
Expires = authCookie.Expires
};
Response.Cookies.Add(newCookie);
return RedirectToLocal(returnUrl);
}
WebSecurity.Logout();
ModelState.AddModelError("UserName", "This account has been deactivated.");
return View(model);
}
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
Notice the newAuthTicket creation and how user instance is passed to it as a JSON. After that all I have to do is desirialize this user object in my base controller's OnAuthorization method:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var principal = new CustomPrincipal(HttpContext.User.Identity)
{
CurrentUserInfo = JsonConvert.DeserializeObject<User>(authTicket.UserData)
};
HttpContext.User = principal;
AppUser = principal.CurrentUserInfo;
ViewBag.AppUser = AppUser;
}
}
base.OnAuthorization(filterContext);
}
Create a new table in your database.