ASP.NET MVC creating context instance inside ActionFilter - asp.net-mvc

The system is built in ASP.NET MVC and has a number of ActionFilters that deal with various features such as if the current logged on user has permission to view a page or if the system is in maintenance mode etc. All of these ActionFilters inherit from a base ActionFilter that has a method for retrieving the current logged on user, it cannot persist the user as their permissions may get changed while they're logged in. Therefore, the DB has to be called the retrieve the logged on user.
Previously the code looked like this:
public class BaseRedirectingAction : ActionFilterAttribute
{
private readonly IUserAuthenticationRepository _userAuthenticationRepository = new UserAuthenticationRepository();
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
}
internal void Redirect(ActionExecutingContext context, RouteValueDictionary keyValues)
{
context.Result = new RedirectToRouteResult(keyValues);
context.Result.ExecuteResult(context.Controller.ControllerContext);
}
internal User GetCurrentUser()
{
if (HttpContext.Current.User.Identity is ClaimsIdentity identity)
{
var claims = identity.Claims;
var userIdClaim = claims.FirstOrDefault(x => x.Type.ToLower() == "userid");
var userAuthenticationIdClaim = claims.FirstOrDefault(x => x.Type.ToLower() == "userauthenticationid");
if (userIdClaim != null)
{
var userAuthenticationId = int.Parse(userAuthenticationIdClaim.Value);
// Declared repository caching value so doesn't pick up updates
var userAuthentication = _userAuthenticationRepository.FindOne(x => x.Id == userAuthenticationId);
var currentLoggedInUser = userIdClaim != null ? userAuthentication.Users.FirstOrDefault(x => x.Id == int.Parse(userIdClaim.Value)) : userAuthentication.Users.FirstOrDefault(x => x.DefaultAccount);
return currentLoggedInUser;
}
}
return null;
}
}
Due to the DB request being cached it had to be changed to:
public class BaseRedirectingAction : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
}
internal void Redirect(ActionExecutingContext context, RouteValueDictionary keyValues)
{
context.Result = new RedirectToRouteResult(keyValues);
context.Result.ExecuteResult(context.Controller.ControllerContext);
}
internal User GetCurrentUser()
{
if (HttpContext.Current.User.Identity is ClaimsIdentity identity)
{
var claims = identity.Claims;
var userIdClaim = claims.FirstOrDefault(x => x.Type.ToLower() == "userid");
var userAuthenticationIdClaim = claims.FirstOrDefault(x => x.Type.ToLower() == "userauthenticationid");
if (userIdClaim != null)
{
var userAuthenticationId = int.Parse(userAuthenticationIdClaim.Value);
// Locally created doesn't cause cached result
var userAuthenticationRepository = new UserAuthenticationRepository();
var userAuthentication = userAuthenticationRepository.FindOne(x => x.Id == userAuthenticationId);
var currentLoggedInUser = userIdClaim != null ? userAuthentication.Users.FirstOrDefault(x => x.Id == int.Parse(userIdClaim.Value)) : userAuthentication.Users.FirstOrDefault(x => x.DefaultAccount);
return currentLoggedInUser;
}
}
return null;
}
}
Will instantiating a new repository each time cause memory leaks or other issues?

Related

Identity Server - how to I include with token additional claims (from external identity provider)

I am having a hard time finding solid information about how to send additional claims to my client application from identity server.
At the current moment, I am using the following to get a local claim (that I captured within ProcessLoginCallbackForOidc) and added to the claims being returned during authentication.
Is this the best approach?
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//var sub = context.Subject.GetSubjectId();
var claims = context.Subject.Claims.ToList();
if (claims.Count > 0)
{
var emailClaim = claims.FirstOrDefault(x => x.Type == "email");
if (emailClaim == null)
{
var emailAddressClaim = context.Subject.Claims.FirstOrDefault(x => x.Type == "emails");
if (emailAddressClaim != null)
{
claims.Add(new Claim("email", emailAddressClaim.Value));
}
}
}
// Set returned claims (System.Security.Claims.Claim) by setting context.IssuedClaims
context.IssuedClaims = claims;
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
Yes this is proper solution. ProfileService is an extensibility point to add extra claims for a user.
Its called when creating token for the user.
Read more here
Edit:
Here is sample code for profile service:
public class ProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
{
_userManager = userManager;
_claimsFactory = claimsFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
if (claims.Count > 0)
{
var emailClaim = claims.FirstOrDefault(x => x.Type == "email");
if (emailClaim == null)
{
var emailAddressClaim = context.Subject.Claims.FirstOrDefault(x => x.Type == "emails");
if (emailAddressClaim != null)
{
claims.Add(new Claim("email", emailAddressClaim.Value));
}
}
}
context.IssuedClaims = claims;
return Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}

Custom Authorization with Parameters Web API

Can someone show me how to use the parameter in Customize AuthorizeAttribute?
Like this:
[Authorize(Role="Admin,Supervisor")]
[Authorize(User="Me,You")]
[Authorize(Action="abc,def")]
This is my code now and I dont have any idea yet how to add the parameter here.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
ApplicationDbContext _context = new ApplicationDbContext();
public override void OnAuthorization(HttpActionContext actionContext)
{
if (AuthorizeRequest(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
if (((System.Web.HttpContext.Current.User).Identity).IsAuthenticated)
{
actionContext.Response = new HttpResponseMessage()
{
StatusCode = HttpStatusCode.Unauthorized,
Content = new StringContent("You are unauthorized to access this resource")
};
}
else
{
base.HandleUnauthorizedRequest(actionContext);
}
}
private bool AuthorizeRequest(HttpActionContext actionContext)
{
var action = actionContext.ActionDescriptor.ActionName;
var controller = actionContext.ControllerContext.ControllerDescriptor.ControllerName;
var currentUser = actionContext.RequestContext.Principal.Identity.GetUserId();
var user = _context.Users.Join(_context.UserAccesses, x => x.RoleId, y => y.RoleId, (x, y) =>
new { Id = x.Id, firstName = x.firstName, lastName = x.lastName, RoleId = x.RoleId, Controller = y.Controller,
Action = y.Action }).Where(z => z.Id == currentUser && z.Controller == controller && z.Action == action)
.SingleOrDefault();
if (user != null)
return true;
else
return false;
}
}
As you have extended the default implementation of Authorize, you need to use [CustomAuthorize(Role="Admin,Supervisor")]. This will set the roles. You can then access the Roles property directly in your code as they are contained in the parent AuthorizeAttribute which has been inherited.
public override void OnAuthorization(HttpActionContext actionContext)
{
var roles = Roles;
if (AuthorizeRequest(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}

nopCommerce Error: Child actions are not allowed to perform redirect actions

I'm using MVC nopCommerce and developing custom plugin which override existing functionality of HomepageBestSellers (action of ProductController which is attributed as [ChildActionOnly]).
FilterProvider:
namespace Nop.Plugin.Product.BestSellers.Filters
{
public class BestSellersFilterProvider : IFilterProvider
{
private readonly IActionFilter _actionFilter;
public BestSellersFilterProvider(IActionFilter actionFilter)
{
_actionFilter = actionFilter;
}
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
if (actionDescriptor.ControllerDescriptor.ControllerType == typeof(ProductController) && actionDescriptor.ActionName.Equals("HomepageBestSellers"))
{
return new Filter[]
{
new Filter(_actionFilter, FilterScope.Action, null)
};
}
return new Filter[] { };
}
}
}
Action Filter:
namespace Nop.Plugin.Product.BestSellers.Filters
{
public class BestSellersFilter : ActionFilterAttribute
{
private readonly ISettingService _settingService;
private readonly IStoreService _storeService;
private readonly IWorkContext _workContext;
public BestSellersFilter(ISettingService settingService,
IStoreService storeService, IWorkContext workContext)
{
this._settingService = settingService;
this._storeService = storeService;
this._workContext = workContext;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//load settings for a chosen store scope and ensure that we have 2 (or more) stores
var storeScope = 0;
if (_storeService.GetAllStores().Count < 2)
storeScope = 0;
var storeId = _workContext.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.AdminAreaStoreScopeConfiguration);
var store = _storeService.GetStoreById(storeId);
storeScope = store != null ? store.Id : 0;
var bestSellersSettings = _settingService.LoadSetting<BestSellersSettings>(storeScope);
if (bestSellersSettings.IsBestSellersEnabled)
{
filterContext.Result = new RedirectResult("Plugins/BestSellersProducts/PublicInfo");
}
else
base.OnActionExecuting(filterContext);
}
}
}
I am getting the following error on filterContext.Result = new RedirectResult("Plugins/BestSellersProducts/PublicInfo"); this line:
Child actions are not allowed to perform redirect actions.
Description: An unhandled exception occurred during the execution of the current web request.
Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: Child actions are not allowed to perform redirect actions.
UPDATE:
Changed BestSellersFilter.cs according to answer.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var storeScope = 0;
if (_storeService.GetAllStores().Count < 2)
storeScope = 0;
var storeId = _workContext.CurrentCustomer.GetAttribute<int>(SystemCustomerAttributeNames.AdminAreaStoreScopeConfiguration);
var store = _storeService.GetStoreById(storeId);
storeScope = store != null ? store.Id : 0;
var featuredProductsSettings = _settingService.LoadSetting<FeaturedProductsSettings>(storeScope);
if (featuredProductsSettings.IsFeaturedProductsEnabled)
{
var products = _productService.GetAllProductsDisplayedOnHomePage();
BestSellersController objResult = new BestSellersController();
filterContext.Result = new ContentResult { Content = objResult.PublicInfoPlugin() };
//base.OnActionExecuting(filterContext);
}
else
base.OnActionExecuting(filterContext);
}
Changed BestSellersController.cs according to answer.
public string PublicInfoPlugin()
{
var featuredProductsSettings = _settingService.LoadSetting<FeaturedProductsSettings>(_storeContext.CurrentStore.Id);
if (featuredProductsSettings.IsFeaturedProductsEnabled)
{
var products = _productService.GetAllProductsDisplayedOnHomePage();
//ACL and store mapping
products = products.Where(p => _aclService.Authorize(p) && _storeMappingService.Authorize(p)).ToList();
//availability dates
products = products.Where(p => p.IsAvailable()).ToList();
if (products.Count == 0)
return "";
var model = PrepareProductOverviewModels(products.Take(featuredProductsSettings.ShowFeaturedProductsNumber)).ToList();
return RenderPartialViewToString("~/Plugins/Product.FeaturedProducts/Views/ProductFeaturedProducts/PublicInfo.cshtml", model);
}
return "";
}
Now getting null values from all the private objects in PublicInfoPlugin method.
Instead of a RedirectResult, set it to a ContentResult and set the Content property to the output from your plugin's action:
filterContext.Result = new ContentResult { Content = "Load your plugin's action here" };

Authentication in webAPI

I want to set Individual authentication for the web API application i have created in Visual studio 2013 using Asp.net . please tell me how can i do that .
VS 2013 by default provide several types of authentication while designing . i choose individual Authentication . But don't know how it works .
Create authentication token on server-side and store it in your database or even in cache. Then send this token with requests from your win forms application. WebApi should check this token all the time. It's good enough and you have full control over your auth process.
Basically it's similar to Darin's answer.
Let me share, how it works for me:
Object with Auth details:
public class TokenIdentity
{
public int UserID { get; set; }
public string AuthToken { get; set; }
public ISocialUser SocialUser { get; set; }
}
Web API Auth Controller:
public class AuthController : ApiController
{
public TokenIdentity Post(
SocialNetwork socialNetwork,
string socialUserID,
[FromUri]string socialAuthToken,
[FromUri]string deviceRegistrationID = null,
[FromUri]DeviceType? deviceType = null)
{
var socialManager = new SocialManager();
var user = socialManager.GetSocialUser(socialNetwork, socialUserID, socialAuthToken);
var tokenIdentity = new AuthCacheManager()
.Authenticate(
user,
deviceType,
deviceRegistrationID);
return tokenIdentity;
}
}
Auth Cache Manager:
public class AuthCacheManager : AuthManager
{
public override TokenIdentity CurrentUser
{
get
{
var authToken = HttpContext.Current.Request.Headers["AuthToken"];
if (authToken == null) return null;
if (HttpRuntime.Cache[authToken] != null)
{
return (TokenIdentity) HttpRuntime.Cache.Get(authToken);
}
return base.CurrentUser;
}
}
public int? CurrentUserID
{
get
{
if (CurrentUser != null)
{
return CurrentUser.UserID;
}
return null;
}
}
public override TokenIdentity Authenticate(
ISocialUser socialUser,
DeviceType? deviceType = null,
string deviceRegistrationID = null)
{
if (socialUser == null) throw new ArgumentNullException("socialUser");
var identity = base.Authenticate(socialUser, deviceType, deviceRegistrationID);
HttpRuntime.Cache.Add(
identity.AuthToken,
identity,
null,
DateTime.Now.AddDays(7),
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null);
return identity;
}
}
Auth Manager:
public abstract class AuthManager
{
public virtual TokenIdentity CurrentUser
{
get
{
var authToken = HttpContext.Current.Request.Headers["AuthToken"];
if (authToken == null) return null;
using (var usersRepo = new UsersRepository())
{
var user = usersRepo.GetUserByToken(authToken);
if (user == null) return null;
return new TokenIdentity
{
AuthToken = user.AuthToken,
SocialUser = user,
UserID = user.ID
};
}
}
}
public virtual TokenIdentity Authenticate(
ISocialUser socialUser,
DeviceType? deviceType = null,
string deviceRegistrationID = null)
{
using (var usersRepo = new UsersRepository())
{
var user = usersRepo.GetUserBySocialID(socialUser.SocialUserID, socialUser.SocialNetwork);
user = (user ?? new User()).CopyFrom(socialUser);
user.AuthToken = System.Guid.NewGuid().ToString();
if (user.ID == default(int))
{
usersRepo.Add(user);
}
usersRepo.SaveChanges();
return new TokenIdentity
{
AuthToken = user.AuthToken,
SocialUser = user,
UserID = user.ID
};
}
}
}
Global Action Filter:
public class TokenAuthenticationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (actionContext.Request.RequestUri.AbsolutePath.Contains("api/auth"))
{
return;
}
var authManager = new AuthCacheManager();
var user = authManager.CurrentUser;
if (user == null)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
//Updates the authentication
authManager.Authenticate(user.SocialUser);
}
}
Global.asax registration:
GlobalConfiguration.Configuration.Filters.Add(new AuthFilterAttribute());
The idea is that AuthCacheManager extends AuthManager and decorates it's methods and properties. If there is nothing inside cache then go check database.
It is a little complicated! By default it is a Token-Based authenctication. Check these links for more details :
Individual Accounts in ASP.NET Web API: http://www.asp.net/vnext/overview/authentication/individual-accounts-in-aspnet-web-api
Understanding OWIN Forms authentication options : http://blogs.msdn.com/b/webdev/archive/2013/07/03/understanding-owin-forms-authentication-in-mvc-5.aspx#_Understanding_OWIN_Forms
also these links will help :
10 Things You Should Know about Tokens: http://blog.auth0.com/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies
Cookies vs Tokens. : http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/

What is the correct way of find out if user is logged in in MVC WEB API?

I am very confused about this problem. Restfull service make it up to you to decide which way to implement this functionallity.
Ive read multiple articles about this problem, but every article says something different.
For example some people propopse sessions, but if you do that Web api is losing its "rest fullness". Other people suggest cockies.
I dont know if what i am done is actually done right:
On login of user i create a cockie which contains UserID(Guid) and on every request which needs user to be logged in i check if this id exsists in the DB.
Is it secure enough? Or how should i make it more secure? Or do i have to choose completly different way?
Just create authentication token on server-side and store it in your database or even in cache. Then send this token with requests from your client application. WebApi should check this token all the time. It's good enough and you have full control over your auth process.
Let me share, how it works for me:
Object with Auth details:
public class TokenIdentity
{
public int UserID { get; set; }
public string AuthToken { get; set; }
public ISocialUser SocialUser { get; set; }
}
Web API Auth Controller:
public class AuthController : ApiController
{
public TokenIdentity Post(
SocialNetwork socialNetwork,
string socialUserID,
[FromUri]string socialAuthToken,
[FromUri]string deviceRegistrationID = null,
[FromUri]DeviceType? deviceType = null)
{
var socialManager = new SocialManager();
var user = socialManager.GetSocialUser(socialNetwork, socialUserID, socialAuthToken);
var tokenIdentity = new AuthCacheManager()
.Authenticate(
user,
deviceType,
deviceRegistrationID);
return tokenIdentity;
}
}
Auth Cache Manager:
public class AuthCacheManager : AuthManager
{
public override TokenIdentity CurrentUser
{
get
{
var authToken = HttpContext.Current.Request.Headers["AuthToken"];
if (authToken == null) return null;
if (HttpRuntime.Cache[authToken] != null)
{
return (TokenIdentity) HttpRuntime.Cache.Get(authToken);
}
return base.CurrentUser;
}
}
public int? CurrentUserID
{
get
{
if (CurrentUser != null)
{
return CurrentUser.UserID;
}
return null;
}
}
public override TokenIdentity Authenticate(
ISocialUser socialUser,
DeviceType? deviceType = null,
string deviceRegistrationID = null)
{
if (socialUser == null) throw new ArgumentNullException("socialUser");
var identity = base.Authenticate(socialUser, deviceType, deviceRegistrationID);
HttpRuntime.Cache.Add(
identity.AuthToken,
identity,
null,
DateTime.Now.AddDays(7),
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null);
return identity;
}
}
Auth Manager:
public abstract class AuthManager
{
public virtual TokenIdentity CurrentUser
{
get
{
var authToken = HttpContext.Current.Request.Headers["AuthToken"];
if (authToken == null) return null;
using (var usersRepo = new UsersRepository())
{
var user = usersRepo.GetUserByToken(authToken);
if (user == null) return null;
return new TokenIdentity
{
AuthToken = user.AuthToken,
SocialUser = user,
UserID = user.ID
};
}
}
}
public virtual TokenIdentity Authenticate(
ISocialUser socialUser,
DeviceType? deviceType = null,
string deviceRegistrationID = null)
{
using (var usersRepo = new UsersRepository())
{
var user = usersRepo.GetUserBySocialID(socialUser.SocialUserID, socialUser.SocialNetwork);
user = (user ?? new User()).CopyFrom(socialUser);
user.AuthToken = System.Guid.NewGuid().ToString();
if (user.ID == default(int))
{
usersRepo.Add(user);
}
usersRepo.SaveChanges();
return new TokenIdentity
{
AuthToken = user.AuthToken,
SocialUser = user,
UserID = user.ID
};
}
}
}
Global Action Filter:
public class TokenAuthenticationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (actionContext.Request.RequestUri.AbsolutePath.Contains("api/auth"))
{
return;
}
var authManager = new AuthCacheManager();
var user = authManager.CurrentUser;
if (user == null)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
//Updates the authentication
authManager.Authenticate(user.SocialUser);
}
}
Global.asax registration:
GlobalConfiguration.Configuration.Filters.Add(new AuthFilterAttribute());
The idea is that AuthCacheManager extends AuthManager and decorates it's methods and properties. If there is nothing inside cache then go check database.
It's an example from real app, but I hope the idea is clear :)

Resources