I have a Singleton model class in my MVC application to determine if the user logging in has authorization/admin (based on memberships to certain AD groups). This model class needs to be a Singleton so that the user's access rights can be established once at first logon and used throughout the session:
public sealed class ApplicationUser
{
// SINGLETON IMPLEMENTATION
// from http://csharpindepth.com/articles/general/singleton.aspx#lazy
public static ApplicationUser CurrentUser { get { return lazy.Value; } }
private static readonly Lazy<ApplicationUser> lazy =
new Lazy<ApplicationUser>(() => new ApplicationUser());
private ApplicationUser()
{
GetUserDetails(); // determine if user is authorized/admin
}
// Public members
public string Name { get { return name; } }
public bool IsAuthorized { get { return isAuthorized; } }
public bool IsAdmin { get { return isAdmin; } }
// Private members
// more code
}
The Singleton is instantiated for the first time in my EntryPointController that all other controllers derive from:
public abstract class EntryPointController : Controller
{
// this is where the ApplicationUser class in instantiated for the first time
protected ApplicationUser currentUser = ApplicationUser.CurrentUser;
// more code
// all other controllers derive from this
}
This patterns allows me to use ApplicationUser.CurrentUser.Name or ApplicationUser.CurrentUser.IsAuthorized etc all over my application.
However, the problem is this:
The Singleton holds the reference of the very first user that logs in at the launch of the web application! All subsequent users who log in see the name of the earliest logged-in user!
How can I make the Singleton session specific?
I think you are looking for the Multiton pattern, where each instance is linked to a key.
An example from here
http://designpatternsindotnet.blogspot.ie/2012/07/multiton.html
using System.Collections.Generic;
using System.Linq;
namespace DesignPatterns
{
public class Multiton
{
//read-only dictionary to track multitons
private static IDictionary<int, Multiton> _Tracker = new Dictionary<int, Multiton> { };
private Multiton()
{
}
public static Multiton GetInstance(int key)
{
//value to return
Multiton item = null;
//lock collection to prevent changes during operation
lock (_Tracker)
{
//if value not found, create and add
if(!_Tracker.TryGetValue(key, out item))
{
item = new Multiton();
//calculate next key
int newIdent = _Tracker.Keys.Max() + 1;
//add item
_Tracker.Add(newIdent, item);
}
}
return item;
}
}
}
I got it working with a mixed Singleton-Multiton approach (thanks #Kickaha for the Multiton pointer).
public sealed class ApplicationUser
{
// SINGLETON-LIKE REFERENCE TO CURRENT USER ONLY
public static ApplicationUser CurrentUser
{
get
{
return GetUser(HttpContext.Current.User.Identity.Name);
}
}
// MULTITON IMPLEMENTATION (based on http://stackoverflow.com/a/32238734/979621)
private static Dictionary<string, ApplicationUser> applicationUsers
= new Dictionary<string, ApplicationUser>();
private static ApplicationUser GetUser(string username)
{
ApplicationUser user = null;
//lock collection to prevent changes during operation
lock (applicationUsers)
{
// find existing value, or create a new one and add
if (!applicationUsers.TryGetValue(username, out user))
{
user = new ApplicationUser();
applicationUsers.Add(username, user);
}
}
return user;
}
private ApplicationUser()
{
GetUserDetails(); // determine current user's AD groups and access level
}
// REST OF THE CLASS CODE
public string Name { get { return name; } }
public bool IsAuthorized { get { return isAuthorized; } }
public bool IsAdmin { get { return isAdmin; } }
private string name = HttpContext.Current.User.Identity.Name;
private bool isAuthorized = false;
private bool isAdmin = false;
// Get User details
private void GetUserDetails()
{
// Check user's AD groups and determine isAuthorized and isAdmin
}
}
No changes to my model and controllers.
The current user's object is instantiated in the EntryPointController:
public abstract class EntryPointController : Controller
{
// this is where the ApplicationUser class in instantiated for the first time
protected ApplicationUser currentUser = ApplicationUser.CurrentUser;
// more code
// all other controllers derive from this
}
In my model and everywhere else, I can access the current user's properties using ApplicationUser.CurrentUser.Name or ApplicationUser.CurrentUser.IsAuthorized etc.
How can I make the Singleton session specific?
Will lead to your problem below.
The Singleton holds the reference of the very first user that logs in
at the launch of the web application! All subsequent users who log in
see the name of the earliest logged-in user!
I think you just simply need to store your ApplicationUser object in session per user.
The mechanism should look like this:
Create an instance of your ApplicationUser every authenticated user.
Store ApplicationUser instance in a session with key. ( Don't worry about same key per user because ASP.NET HttpSessionState will handle it for you. )
If you want to access your ApplicationUser object per user just simply get it from HttpSessionState.
You have an option to create/re-create your session in Session_OnStart or in your base controller.
Setup your session setting if you want it to expire or not.
I hope this solution will make sense to you. :)
Related
Here is the problem
A multi-layered approach. The end point/action used BLL Provider which must trim data not only using certain business criteria but also security criteria. For example, a super user can view all items and plain user can only view items assigned to his group.
The knee-jerk reaction to this - public IEnumerable<Item> GetItems(int? userId, string color, string location), where if user Id is not provided - get all items. But what if I want to put additional layer of protection - derive query based on a special MyIdentityProvider.
public class BllItemDataProvider : IBllItemDataProvider
{
private IMyIdentityProvider _myIdentity;
public BllItemDataProvider(IMyIdentityProvider myIdentity)
{
_myIdentity = myIdentity;
}
public BllItemDataProvider(IMyIdentityProvider myIdentity)
{
_myIdentity = myIdentity;
}
}
MyIdentityProvider would have userId, isSuperUser, other flags, etc.
The question is, how to wire it up so that in Web API IHttpContextAccessor or the HttpContext will hydrate MyIdentityProvider from the Claims I find in the [controller].HttpContext.User.Claims. So my controller would look like
public class ItemController : ControllerBase
{
private IBllItemDataProvider _prov
public ItemController (IBllItemDataProvider prov)
{
_prov = prov;
}
[HttpGet("[action]/{color}/{location?}")]
public IActionResult SomeItems (string color, string location)
{
_prov = prov.GetItems(color, location);
}
}
Or, I should just create a base controller which can obtain that and inherit from it?
public abstract class ClaimControllerBase: ControllerBase - in constructor parse claims and setup thread principal, which can then be accessed down in the pipeline?
Thanks
Additionally I can add that these flags in the MyIdentityProvider will be taken from HttpContext.User.Claims which in turn come from JWT token and filled by Identity Framework. So, literally, I can take those claims and slap it in the base controller into a thread. But I don't think this a pretty solution.
Alright, looks like this one goes into DIY, or solve it yourself
Added this code
// CREATED Helper
public class MyIdentityProviderHelper
{
public static IMyIdentityProvider FetchProvider(IHttpContextAccessor accessor)
{
var prov = new MyIdentityProvider();
// -- HERE parse claims and fill properties
return prov;
}
}
// ADDED to Startup
services.AddHttpContextAccessor();
services.AddTransient<IMyIdentityProvider>((s) => // factory method is the real secret here
{
return MyIdentityProviderHelper.FetchProvider(
s.GetService<IHttpContextAccessor>());
});
services.AddTransient<IItemProvider, RealItemProvider>();
// DECLARED RealItemProvider
public class RealItemProvider : IItemProvider
{
private IMyIdentityProvider _identity
public RealItemProvider (IMyIdentityProvider identity)
{
_identity = identity;
}
public IEnumerable<Item> GetItems(string color, string location)
{
IEnumerable<Item> items = null;
if (_identity.Roles.Contains(Roles.SysAdmin))
items = GetAllItems(color, location); // private
else
items = GetUserItems(color, location); // private
return items;
}
}
As you see, the main soul saver here is ability to provide a factory for the object and use factory of another object to pass the retrieved object
Given the following masterpage or content page ...............
namespace Intranet
{
public partial class Site : System.Web.UI.MasterPage
{
WebSite.Security.Users.CurrentUser currentUser;
protected void Page_Init(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
currentUser = new WebSite.Security.Users.CurrentUser();
currentUser = WebSite.Security.Users.GetCurrentUser(currentUser);
Label_UserId.Text = currentUser.UserId;
}
}
}
}
that calls the following ............
namespace Intranet
{
public class WebSite
{
public class Security
{
public class Users
{
public class CurrentUser
{
public string UserId { get; set; }
}
public static CurrentUser GetCurrentUser(CurrentUser cu)
{
cu.UserId = "MethodThatGetsUserId";
return cu;
}
}
}
}
}
Will the returned instantiated class 'currentUser' contain unique information even if several different users are on the page at the same time?
Thanks for your time and insight.
Yes, a new class is instantiated for each request even, not just each user.
Static fields in the class will be shared, and you should use session and application data to share data across requests or users.
Nope, with this line:
currentUser = new WebSite.Security.Users.CurrentUser();
You are creating a new instance in your master page class. Instances created in each request are only available in that request (of course, depending on the scope), unless you use static variables. Static variables are the same for all users/threads in your application.
However, what you actually want to do is to get the current user. This should be done using the HttpContext.Current.User or Page.Current which is an IPrincipal and should contain information you filled in the Authenticate_Request method of your application.
To understand more about the ASP.NET forms authentication, please refer to: http://msdn.microsoft.com/en-us/library/9wff0kyh(v=vs.100).aspx
When I'm putting following code:
#using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm" }))
{
#Html.AntiForgeryToken()
Log off
}
the
#Html.AntiForgeryToken()
part is thrownig following exception:
The provided identity of type 'System.Web.Security.FormsIdentity' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.
I've checked many examples and tried to search the web, but I cannot find any explanation. I would like to know why this error happens to me? And how to solve it to use antiforgery.
It's telling you that it won't work because despite being logged in, Membership.GetUser().UserName is not providing a name that can be used for hashing.
So your real problem is, "How come my logged in user doesn't have a username?"
There can be some cases when logged in user doesn't have Identity.Name set (in my case I have to integrate my app with some crazy log-in system). Then there are two ways around:
1) unsecure - all users will be treated the same way by antiforgery system regardless of their auth status
// System.Web.WebPages.dll
using System.Web.Helpers;
// not a production solution
public class MvcApplication : HttpApplication {
protected void Application_Start() {
AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;
}
}
2) secure - provide your own (custom) way how you distinguish your users
using System;
using System.Globalization;
using System.Web;
using System.Web.Helpers;
public class ContoscoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider {
public string GetAdditionalData(HttpContextBase context) {
if (context == null) {
throw new ArgumentNullException("context");
}
var contoscoContext = new ContoscoHttpContext(context);
int userID = contoscoContext.GetUserID().GetValueOrDefault();
return Convert.ToString(userID, CultureInfo.InvariantCulture);
}
public bool ValidateAdditionalData(HttpContextBase context, string additionalData) {
string data = GetAdditionalData(context);
return string.Compare(data, additionalData, StringComparison.Ordinal) == 0;
}
}
public class MvcApplication : HttpApplication {
protected void Application_Start() {
AntiForgeryConfig.AdditionalDataProvider =
new ContoscoAntiForgeryAdditionalDataProvider();
}
}
where ContoscoHttpContext is class that returns UserID (or any unique user token) based on current context (i.e. HttpContextBase):
public class ContoscoHttpContext {
private HttpContextBase _context;
public ContoscoHttpContext(HttpContextBase context) {
_context = context;
}
public int? GetUserID() {
// TODO: provide your own implementation how to get user id
// based on HttpContextBase stored in _context
// in my case it was something like
// return ((ContoscoPrincipal)_context.User).UserID;
}
}
I have a class declaration, public abstract class CompanyHttpApplication : HttpApplication
In the CompanyHttpApplication I have
public static CompanyHttpApplication Current
{
get { return (CompanyHttpApplication)HttpContext.Current.ApplicationInstance; }
}
and
public CompanyProfileInfo Profile
{
get
{
return profile ?? (profile = ProfileManager
.FindProfilesByUserName(ProfileAuthenticationOption.Authenticated,
User.Identity.Name).Cast<CompanyProfileInfo>().ToList().First());
}
private set { profile = value; }
}
Whenever I access the user:
CatapultHttpApplication.Current.Profile.UserName
it gets whoever is logged in last. I understand why (because it's in the application instead of the session), but I don't know how to change it. What can I do to change it so it uses the session instead of the application?
I have an object that contains all login data, that's in my controller (it was programmed before switching to MVC3).
I'm trying to add authorization to the site, so so far I have:
public LoginObject MyLoginObject
{
get;
set;
}
[CustomAuthorization()]
public ActionResult Index()
{
return View();
}
and
public class CustomAuthorization : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return true;
//should be return myLoginObject.IsLoggedIn;
}
}
Is there anyway to pass MyLoginObject into the AuthorizeAttribute class? If not could I at least pass in a boolean from the object that specifies if the user is authorized or not?
Edit: My solution based on Zonnenberg's advice.
public class LoginObject : IPrincipal // Now extends IPrincipal
{
... //old code
private class IdentityImpl : IIdentity
{
public string AuthenticationType
{
get;
set;
}
public bool IsAuthenticated
{
get;
set;
}
public string Name
{
get;
set;
}
}
public IIdentity Identity
{
get { return new IdentityImpl { AuthenticationType = "Custom Authentication", IsAuthenticated = this.IsLoggedIn, Name = this.Id}; }
}
}
Then I moved the instantiation of loginobject into CustomAuthorization
public override void OnAuthorization(AuthorizationContext filterContext)
{
// ... Set up LoginObject
filterContext.RequestContext.HttpContext.User = myLoginObject;
base.OnAuthorization(filterContext);
}
So now logging in, is done inside the authorization, and I can call User to access the login from the controller.
You can check wheter the user is logged in by using httpContext.User.Identity.IsAuthenticated.
To store more information you could use the httpContext.User object. You can write your own implementation of IPrincipal and IIdentity to store all kinds of login information.
Other option is to store login info in the Session.
How is your LoginObject instantiated?
If it's instantiated via a service or repository (ex. MyLoginObject = loginService.GetLogin() then you can move this call into the CustomAuthorization attribute.
If the logic is within the controller itself then this should be refactored into a service or repository depending on you solution architecture so that you can do the above.