I am trying to create a Session Wrapper class for ASP.NET Core Razor Pages.
In traditional ASP.NET web forms my singleton session wrapper class used to work normally, but i am not sure if in .net core it will work the same or the same class will be shared across all requests.
I want to store specific information for each request (session) and not to be shared across all requests.
i created the following wrapper :
public class MySession
{
//the ISession interface mandatory
public ISession Session { get; set; }
private static MySession instance;
private MySession() {
}
public static MySession Instance
{
get
{
if (instance == null)
{
instance = new MySession();
}
return instance;
}
}
//properties
public User User
{
get => SessionHelper.SessionExtensions.Get<User>(this.Session, "User");
set => SessionHelper.SessionExtensions.Set<User>(this.Session, "User", value);
}
public bool LoggedIn
{
get => SessionHelper.SessionExtensions.Get<bool>(this.Session, "LoggedIn");
set => SessionHelper.SessionExtensions.Set<bool>(this.Session, "LoggedIn", value);
}
}
and from my page model (Login.cshtml.cs) i am doing the following :
public void OnGet()
{
MySession.Instance.Session = HttpContext.Session;
}
and i am accessing the session and storing and retrieving information perfectly as expected for example :
public ActionResult OnPostAuthenticate()
{
string username = Request.Form["username"];
string password = Request.Form["password"];
User user = this.userDataManager.GetUserAuthentication(username, password);
if (user != null)
{
//authentication success
//save session variables
MySession.Instance.User = user;
MySession.Instance.LoggedIn = true;
return new JsonResult(new { success = true });
}
else
{
return new JsonResult(new { success = false });
}
}
And as i said everything work perfect, but i want to know if it is SAFE to use the session this way or my requests will be messed up and the same session information will be shared across all requests ?
Related
There are many questions about custom authentication in ASP.NET, but none of them answers how to fully integrate it with ASP.NET mechanics.
I want to make a web application for system which already exists and have users.
Let's create new ASP.NET MVC project. I choose "No authentication" to make it from scratch (because I need to read from custom tables, custom hashing function etc, so I'll go this way).
I'll use IIdentity and IPrincipal interfaces to carry logged in user in HttpContext.
public class Identity : IIdentity
{
public Identity(string name)
{
Name = name;
IsAuthenticated = true;
}
public string Name { get; private set; }
public string AuthenticationType { get; set; }
public bool IsAuthenticated { get; set; }
}
public class Principal : IPrincipal
{
public Principal(string email)
{
Identity = new Identity(email);
}
public IIdentity Identity { get; private set; }
public bool IsInRole(string role)
{
return false;
}
}
I'll create SessionsController which will create and destroy Session. Session will contain Id of logged in user.
public class UserManager
{
public bool Authenticate(WorkerDTO user, string password)
{
using (var context = new DatabaseContext())
{
var user = context.Users.SingleOrDefault(w => w.Email == user.Email);
if (user != null)
{
// compute SHA512 of password with salt
var hash = Hash(user.Password);
if (user.HashPassword == hash)
return true;
}
return false;
}
}
}
public class SessionsController : Controller
{
private IDatabaseManager _dbManager;
private UserManager _userManager;
public SessionsController(IDatabaseManager dbManager, UserManager userManager)
{
_dbManager = dbManager;
_userManager = userManager;
}
[HttpGet]
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(UserLoginViewModel model)
{
var user = _dbManager.Give(new WorkerByEmail(model.Email));
if(user != null && _userManager.Authenticate(user, model.Password))
{
// create session
Session["UserId"] = worker.Id;
// assign User property
User = new Principal(worker.Email);
return RedirectToAction("Index", "Home");
}
return RedirectToAction("New");
}
public ActionResult Logout()
{
Session.Remove("UserId");
User = null;
return View();
}
}
Application won't remember that User = new Principal(worker.Email);.
So, if I want to make use of sessions, I want to tell my ASP.NET application that User behind UserId carried by session (probably a cookie) each request is the logged in user. In Global.asax I have available events with reasonable names:
AuthenticateRequest
PostAuthenticateRequest.
Unfortunately, Session is unavailable in these events.
How to make use of ASP.NET in my application? Maybe Session isn't the way to go?
Should I create new IPrincipal object and attach to User prop each request?
And why User property isn't null at start? Should I set it to null on logout?
I am using SignalR version 2.1.2 with ASP.Net MVC 5 & NServiceBus and have following requirement
There is a signup page (anonymous authentication) in which SignalR is used to send notifications. Every form submit will generate a new connection id which needs to be kept in a collection so that I can send response to the client. Context.User.Identity.Name is empty hence _connections.Add(name, Context.ConnectionId); cannot be used in OnConnected() hub event as given in this post
Similar problem exists in Login page.
If there is a possibility to control the ConnectionId then I could overcome this situation but it looks like new version of SignalR has got rid of connection factory.
I am using Redis cache so one option is to write my own connection management code to keep these connection ids in it.
Second option is to use Forms Authentication in such a way that a 'Anonymous Role' is assigned to these users which restricts the usage to anonymous views/controllers but gives a 'Name' to the user so that Context.User.Identity.Name is not empty. With this I can use built in SignalR mechanism to manage connection ids for me.
This is what we did in BaseAnonymousController
public class BaseAnonymousController : Controller
{
protected override void OnAuthentication(System.Web.Mvc.Filters.AuthenticationContext filterContext)
{
if (filterContext.Controller.GetType().Name == "AccountController" && filterContext.ActionDescriptor.ActionName == "login")
{
Guid result;
if (!string.IsNullOrEmpty(SessionVariables.UserId) && Guid.TryParse(SessionVariables.UserId, out result))
{
//Already a anonymous user, so good to go.
}
else
{
//Seems to be a logged in a user. So, clear the session
Session.Clear();
}
}
//Perform a false authentication for anonymous users (signup, login, activation etc. views/actions) so that SignalR will have a user name to manage its connections
if (!string.IsNullOrEmpty(SessionVariables.UserId))
{
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(SessionVariables.UserId, "Anonymous"));
}
else
{
string userName = Guid.NewGuid().ToString();
filterContext.HttpContext.User = new CustomPrincipal(new CustomIdentity(userName, "Anonymous"));
FormsAuthentication.SetAuthCookie(userName, false);
SessionVariables.UserId = userName;
}
base.OnAuthentication(filterContext);
}
}
and used this class as base class for all of anonymous controllers.
public class AccountController : BaseAnonymousController
{
[AllowAnonymous]
public ActionResult Signup()
{
//Your code
}
[AllowAnonymous]
public ActionResult Login()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotPassword()
{
//Your code
}
[AllowAnonymous]
public ActionResult ForgotUsername()
{
//Your code
}
}
In the SignalR hub (nothing extraordinary than what is in SignalR documentation)
public override Task OnConnected()
{
SignalRConnectionStore.Add(Context.User.Identity.Name, Context.ConnectionId);
return base.OnConnected();
}
public override Task OnReconnected()
{
string name = Context.User.Identity.Name;
//Add the connection id if it is not in it
if (!SignalRConnectionStore.GetConnections(name).Contains(Context.ConnectionId))
{
SignalRConnectionStore.Add(name, Context.ConnectionId);
}
return base.OnReconnected();
}
public override Task OnDisconnected(bool stopCalled)
{
SignalRConnectionStore.Remove(Context.User.Identity.Name, Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
This works for both anonymous and authenticated users.
SignalRConnectionStore class and Interface
public interface ISignalRConnectionStore
{
int Count { get; }
void Add(string userName, string connectionId);
IEnumerable<string> GetConnections(string userName);
void Remove(string userName, string connectionId);
}
internal class SignalRConnectionStore : ISignalRConnectionStore
{
private readonly Dictionary<string, HashSet<string>> _connections = new Dictionary<string, HashSet<string>>();
public int Count
{
get
{
return _connections.Count;
}
}
public void Add(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
connections = new HashSet<string>();
_connections.Add(userName, connections);
}
lock (connections)
{
connections.Add(connectionId);
}
}
}
}
public IEnumerable<string> GetConnections(string userName)
{
if (!string.IsNullOrEmpty(userName))
{
HashSet<string> connections;
if (_connections.TryGetValue(userName, out connections))
{
return connections;
}
}
return Enumerable.Empty<string>();
}
public void Remove(string userName, string connectionId)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(connectionId))
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(userName, out connections))
{
return;
}
lock (connections)
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.Remove(userName);
}
}
}
}
}
}
Declare a static variable of SignalRConnectionStore in Hub class as below.
public class ProvisioningHub : Hub
{
private static ISignalRConnectionStore SignalRConnectionStore;
public ProvisioningHub(ISignalRConnectionStore signalRConnectionStore)
: base()
{
SignalRConnectionStore = signalRConnectionStore; //Injected using Windsor Castle
}
}
Use Forms Authentication, store a Federated Cookie and store the hub region in the cookie as well..
In SignalR jQuery code, use a jQuery plugin to read HTTP cookie and get the region name and subscribe to notifications.
Alternatively, in your .cshtml, render jQuery with region populated from your View Model.
Note: Use FormsAuthentication.SetAuthCookie as this will create HTTP Only cookie and will be sent in Ajax and non-Ajax calls.
I am building a SAAS application and planned for one database per client. I am using Code First EF6 with ASP.Net MVC 4.
There will be 2 context i.e. MasterContext and TenantContext. User will first hit to MasterContext to authenticate user credentials and fetch its Tenant configuration.
Based on fetched Tenant configuration; TenantContext is set to Tenant specific database and used for Tenant CRUD operations.
Please advice how to achieve this.
The idea is to identify the current request tenant_id and use that to fetch the database configuration and create DbContext like the below code.
public AppDbContext : DbContext
{
private const string _defaultCS = "default app connection string";
public AppDbContext() : base(GetConnectionString())
{
}
private string GetConnectionString()
{
return TenantContext.ConnectionString ?? _defaultCS;
}
}
Sample usage
public class StudentRepo
{
public Student Get(Guid id)
{
using(var ctx = new AppDbContext())
{
return ctx.Students.FirstOrDefault(x=>x.Id == id);
}
}
}
This will automatically connect to the logged in tenant database.
You might need to store tenant_id in Auth cookie and read it after PostAuthenticate_Event and store in HttpContext.Current.Items
public static TenantContext
{
public static Guid TenantId
{
get
{
return (Guid)HttpContext.Current.Items["__TenantID"];
}
}
public static string ConnectionString
{
get
{
return TenantConfigService.GetConnectionString(TenantId);
}
}
}
In some HTTP module Init method
context.PostAuthenticateRequest += context_PostAuthenticateRequest;
void context_PostAuthenticateRequest(object sender, EventArgs e)
{
FormsIdentity identity = Thread.CurrentPrincipal.Identity as FormsIdentity;
if (identity != null)
{
HttpContext.Current.Items["__TenantID"] = GetTenantIdFromTicket(identity.Ticket.UserData); // returns tenant_id as guid type
}
}
I'm using NHibernate on a custom membership provider in a MVC3 application. Whenever I try to login though, I get the exception:
Session is closed!
Object name: 'ISession'
The code in the membership provider looks like this:
ContractRepository repository;
public string UserDescription { get; set; }
public CustomSqlMembershipProvider() {
this.repository = new ContractRepository(ProviderPortal.Persistance.NHibernateSessionStorage.RetrieveSession());
}
public override bool ValidateUser(string username, string password) {
var user = repository.GetContractForUser(username);
if (user == null)
return false;
else {
UserDescription = user.Description;
return true; //TODO: come back and add user validation.
}
}
And here is the retrieve session methods:
public static ISession RetrieveSession() {
HttpContext context = HttpContext.Current;
if (!context.Items.Contains(CURRENT_SESSION_KEY)) OpenCurrent();
var session = context.Items[CURRENT_SESSION_KEY] as ISession;
return session;
}
private static void OpenCurrent() {
ISession session = NHibernateConfiguration.CreateAndOpenSession();
HttpContext context = HttpContext.Current;
context.Items[CURRENT_SESSION_KEY] = session;
}
This is where the exception is happening:
public Contract GetContractForUser(string UserName) {
return (Contract)session.CreateCriteria(typeof(Contract))
.Add(Restrictions.Eq("Login", int.Parse(UserName))).UniqueResult();
}
Somewhere between the CustomSqlMembershipProvider constructor being called, and the ValidateUser method being called, the session is being closed. Any ideas? My other Controllers are injected with an open session via DI, but this one is giving me the hardest time.
Do you get this one consistently or on and off?
We were getting this using Spring.net for our DI and using OpenSessionInView
We had to add the following http module to change the storage options for the current thread.
public class SpringThreadStorageModule : IHttpModule
{
static SpringThreadStorageModule()
{
LogicalThreadContext.SetStorage(new HybridContextStorage());
}
#region IHttpModule Members
public void Dispose()
{
// do nothing
}
public void Init(HttpApplication context)
{
// we just need the staic init block.
}
#endregion
}
I have had some problems with authentication in ASP.NET. I'm not used most of the built in authentication in .NET.
I gotten some complaints from users using Internet Explorer (any version - may affect other browsers as well) that the login process proceeds but when redirected they aren't authenticated and are bounced back to loginpage (pages that require authentication check if logged in and if not redirect back to loginpage). Can this be a cookie problem?
Do I need to check if cookies are enabled by the user?
What's the best way to build authentication if you have a custom member table and don't want to use ASP.NET login controls?
Here my current code:
using System;
using System.Linq;
using MyCompany;
using System.Web;
using System.Web.Security;
using MyCompany.DAL;
using MyCompany.Globalization;
using MyCompany.DAL.Logs;
using MyCompany.Logging;
namespace MyCompany
{
public class Auth
{
public class AuthException : Exception
{
public int StatusCode = 0;
public AuthException(string message, int statusCode) : base(message) { StatusCode = statusCode; }
}
public class EmptyEmailException : AuthException
{
public EmptyEmailException() : base(Language.RES_ERROR_LOGIN_CLIENT_EMPTY_EMAIL, 6) { }
}
public class EmptyPasswordException : AuthException
{
public EmptyPasswordException() : base(Language.RES_ERROR_LOGIN_CLIENT_EMPTY_PASSWORD, 7) { }
}
public class WrongEmailException : AuthException
{
public WrongEmailException() : base(Language.RES_ERROR_LOGIN_CLIENT_WRONG_EMAIL, 2) { }
}
public class WrongPasswordException : AuthException
{
public WrongPasswordException() : base(Language.RES_ERROR_LOGIN_CLIENT_WRONG_PASSWORD, 3) { }
}
public class InactiveAccountException : AuthException
{
public InactiveAccountException() : base(Language.RES_ERROR_LOGIN_CLIENT_INACTIVE_ACCOUNT, 5) { }
}
public class EmailNotValidatedException : AuthException
{
public EmailNotValidatedException() : base(Language.RES_ERROR_LOGIN_CLIENT_EMAIL_NOT_VALIDATED, 4) { }
}
private readonly string CLIENT_KEY = "9A751E0D-816F-4A92-9185-559D38661F77";
private readonly string CLIENT_USER_KEY = "0CE2F700-1375-4B0F-8400-06A01CED2658";
public Client Client
{
get
{
if(!IsAuthenticated) return null;
if(HttpContext.Current.Items[CLIENT_KEY]==null)
{
HttpContext.Current.Items[CLIENT_KEY] = ClientMethods.Get<Client>((Guid)ClientId);
}
return (Client)HttpContext.Current.Items[CLIENT_KEY];
}
}
public ClientUser ClientUser
{
get
{
if (!IsAuthenticated) return null;
if (HttpContext.Current.Items[CLIENT_USER_KEY] == null)
{
HttpContext.Current.Items[CLIENT_USER_KEY] = ClientUserMethods.GetByClientId((Guid)ClientId);
}
return (ClientUser)HttpContext.Current.Items[CLIENT_USER_KEY];
}
}
public Boolean IsAuthenticated { get; set; }
public Guid? ClientId {
get
{
if (!IsAuthenticated) return null;
return (Guid)HttpContext.Current.Session["ClientId"];
}
}
public Guid? ClientUserId {
get {
if (!IsAuthenticated) return null;
return ClientUser.Id;
}
}
public int ClientTypeId {
get {
if (!IsAuthenticated) return 0;
return Client.ClientTypeId;
}
}
public Auth()
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
IsAuthenticated = true;
}
}
public void RequireClientOfType(params int[] types)
{
if (!(IsAuthenticated && types.Contains(ClientTypeId)))
{
HttpContext.Current.Response.Redirect((new UrlFactory(false)).GetHomeUrl(), true);
}
}
public void Logout()
{
Logout(true);
}
public void Logout(Boolean redirect)
{
FormsAuthentication.SignOut();
IsAuthenticated = false;
HttpContext.Current.Session["ClientId"] = null;
HttpContext.Current.Items[CLIENT_KEY] = null;
HttpContext.Current.Items[CLIENT_USER_KEY] = null;
if(redirect) HttpContext.Current.Response.Redirect((new UrlFactory(false)).GetHomeUrl(), true);
}
public void Login(string email, string password, bool autoLogin)
{
Logout(false);
email = email.Trim().ToLower();
password = password.Trim();
int status = 1;
LoginAttemptLog log = new LoginAttemptLog { AutoLogin = autoLogin, Email = email, Password = password };
try
{
if (string.IsNullOrEmpty(email)) throw new EmptyEmailException();
if (string.IsNullOrEmpty(password)) throw new EmptyPasswordException();
ClientUser clientUser = ClientUserMethods.GetByEmailExcludingProspects(email);
if (clientUser == null) throw new WrongEmailException();
if (!clientUser.Password.Equals(password)) throw new WrongPasswordException();
Client client = clientUser.Client;
if (!(bool)client.PreRegCheck) throw new EmailNotValidatedException();
if (!(bool)client.Active || client.DeleteFlag.Equals("y")) throw new InactiveAccountException();
FormsAuthentication.SetAuthCookie(client.Id.ToString(), true);
HttpContext.Current.Session["ClientId"] = client.Id;
log.KeyId = client.Id;
log.KeyEntityId = ClientMethods.GetEntityId(client.ClientTypeId);
}
catch (AuthException ax)
{
status = ax.StatusCode;
log.Success = status == 1;
log.Status = status;
}
finally
{
LogRecorder.Record(log);
}
}
}
}
A classic case of over-engineered Authentication mechanism and on top of it the design is bad.
Exceptions should be out of Auth class but reside in same namespace. Can you imagine how .Net framework would look if Microsoft had created exceptions like this. Always keep it simple, stupid (KISS). It seems you need modular code. Try to be simple yet modular.
Your authentication Client-Keys are static magic-values and you're shipping them with your assemblies. Use SecureString instead of readonly string. Anybody can get hold of it using Reflector. How do you sustain change ad security?
Your code directly refers Current HttpContext object when in fact you could have passed the reference of current context object in client-code that will use this.
RequireClientOfType is int[] - why in the world you want to do this ? I believe it could have been an enum or an immutable struct if at all ever needed.
You are already using FormsAuthentication in your Login() and Logout() which is sufficient to replace your entire Auth. Why do you want to re-invent the wheel if ultimately you are going to use FormsAuthnetication to take care of Auth.
And yes if you cannot revise this design please use FxCop/StyleCop at least to avoid spaghetti-code.
Also you could make class Auth as static and expose functionalities like FormsAuthentication does. And also rename it from Auth to Authentication.
This is a prime candidate for http://thedailywtf.com/
Try using built-in asp.net Forms Authentication (Membership).
You can learn from these videos:
Link1 and Link2
If you want to customize it watch this video:
Link