5

I'm trying to decouple my authentication from my controllers, so I cooked up an AuthenticationService which I inject into my authentication controller (DefaultController) using Ninject. Here's the implementation of AuthenticationService:

public sealed class AuthenticationService {
    private IAuthenticationManager AuthenticationManager { get; set; }
    private UserManager<Employee, int> EmployeeManager { get; set; }

    public AuthenticationService(
        IAuthenticationManager authenticationManager,
        UserManager<Employee, int> employeeManager) {
        this.AuthenticationManager = authenticationManager;
        this.EmployeeManager = employeeManager;
    }

    public bool SignIn(
        CredentialsInput credentials) {
        Employee employee = this.EmployeeManager.Find(credentials.Email, credentials.Password);

        if (employee != null) {
            ClaimsIdentity identityClaim = this.EmployeeManager.CreateIdentity(employee, DefaultAuthenticationTypes.ApplicationCookie);

            if (identityClaim != null) {
                this.AuthenticationManager.SignIn(new AuthenticationProperties(), identityClaim);

                return true;
            }
        }

        return false;
    }

    public void SignOut() {
        this.AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    }
}

And here's how I've got Ninject configured to do the injections:

private static void RegisterServices(
    IKernel kernel) {
    kernel.Bind<IUserStore<Employee, int>>().To<EmployeeStore>().InRequestScope();
    kernel.Bind<IRoleStore<Role, int>>().To<RoleStore>().InRequestScope();
    kernel.Bind<IAuthenticationManager>().ToMethod(
        c =>
            HttpContext.Current.GetOwinContext().Authentication).InRequestScope();
}

And here's how I'm configuring OWIN:

public sealed class StartupConfig {
    public void Configuration(
        IAppBuilder app) {
        this.ConfigureAuthentication(app);
    }

    public void ConfigureAuthentication(
        IAppBuilder app) {
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/"),
            ExpireTimeSpan = new TimeSpan(0, 60, 0)
        });
    }
}

Oh, and for good measure here's the DefaultController:

public sealed class DefaultController : Controller {
    private AuthenticationService AuthenticationService { get; set; }

    public DefaultController(
        AuthenticationService authenticationService) {
        this.AuthenticationService = authenticationService;
    }

    [HttpPost, AllowAnonymous, ValidateAntiForgeryToken]
    public RedirectToRouteResult Default(
        [Bind(Prefix = "Credentials", Include = "Email,Password")] CredentialsInput credentials) {
        if (base.ModelState.IsValid
            && this.AuthenticationService.SignIn(credentials)) {
            //  Place of error. The IsInRole() method doesn't return
            //  the correct answer because the
            //  IUserStore<>.FindByIdAsync() method is not setting
            //  the correct data.
            if (this.User.IsInRole("Technician")) {
                return this.RedirectToAction<TechniciansController>(
                    c =>
                        c.Default());
            }

            return this.RedirectToAction(
                c =>
                    c.Dashboard());
        }

        return this.RedirectToAction(
            c =>
                c.Default());
    }

    [HttpGet]
    public RedirectToRouteResult SignOut() {
        this.AuthenticationService.SignOut();

        return this.RedirectToAction(
            c =>
                c.Default());
    }
}

Well, it all kind of works. The problem I'm having is that the sessions being set by the UserManager is inconsistent. For example, if I build the application and then run it (debugging or not) this is what happens:

  • Build and run
  • Sign in as user1@email.com
  • Sign out
  • Sign in as user2@email.com
  • On the first post request, user1 is still showing up as the identity
  • On the second post request (refresh), user2 is correctly shown as the identity

Can someone point into why that is happening? Granted I did pull the code in the SignIn and SignOut methods out of the controller where it was and into this AuthenticationService, but I'm not fully convinced it is the cause of the inconsistency. Since all of the assemblies are processed by the MVC app, it should all work the same, right? Help would be greatly appreciated.

UPDATE

Found this while Googling and although it's marked as resolved, I'm a bit confused on what the resolution was. https://katanaproject.codeplex.com/workitem/201 I don't think this has anything to do with my problem anymore.

UPDATE 2

I believe the root of the issue is asynchrony somewhere in the calling pipeline, specifically in the EmployeeStore I'm injecting into UserManager<Employee, int>. I have a custom implementation of the UserStore called EmployeeStore. It implements IQueryableUserStore<Employee, int>, IUserStore<Employee, int>, IUserPasswordStore<Employee, int>, and IUserRoleStore<Employee, int>.

When I'm debugging the FindByIdAsync(int employeeId) method I see that it fires twice for some reason. The first time it fires I see the correct employeeId passed into it. The second time it fires the employeeId is set to 0. This is where it screws up, I think because it doesn't subsequently call the IsInRoleAsync(Employee employee, string roleName) method.

When I refresh the page after it throws an exception that the UserId is not found, the FindByIdAsync(...) method is again called twice, but this time both times the employeeId is correct, and it then proceeds to call the IsInRoleAsync(...) method.

Here's the code for both methods:

public Task<Employee> FindByIdAsync(
    int employeeId) {
    this.ThrowIfDisposed();

    return this.Repository.FindSingleOrDefaultAsync<Employee, int>(employeeId);
}

public Task<bool> IsInRoleAsync(
    Employee employee,
    string roleName) {
    this.ThrowIfDisposed();

    if (employee == null) {
        throw new ArgumentNullException("employee");
    }

    if (String.IsNullOrEmpty(roleName)) {
        throw new ArgumentNullException("roleName");
    }

    return Task.FromResult<bool>(employee.Roles.Any(
        r =>
            (r.Name == roleName)));
}
Gup3rSuR4c
  • 9,145
  • 10
  • 68
  • 126
  • Just for clarity -- ASP.NET Identity has nothing to do with setting the User property in MVC. That's the job of the Katana authentication middleware. ASP.NET Identity manages your users' credentials and identity data in a database. – Brock Allen Mar 23 '14 at 22:31
  • 1
    I would recommend not using Async methods from Task. Async methods from task use a Thread from the IIS Threadpool which means less threads for servicing HTTP requests. Additionally, since the specifics of the methods `IsInRoleAsync()` are for authorization, the calling thread should be waiting for the result anyway before proceeding (granted/denied) which means making the method Async is actually making the process slower. – Erik Philips Mar 25 '14 at 00:39
  • Completely agree with @ErikPhilips. You shouldn't be using a different thread to authenticate. Was just typing that when he beat me to it – Basic Mar 25 '14 at 00:44
  • @ErikPhilips, I agree with you, but all of the methods of the interfaces are Async, so I don't have a choice there. I created my implementation by poking around the EF Identity implementation with ILSpy and mostly matching what they did. The only thing I did differently is use my already existing Repository. That being said, I am using the synchronous extension methods in my sign in and sign out methods, which are just wrappers around the async methods that I presume are supposed to force them to be synchronous? – Gup3rSuR4c Mar 25 '14 at 00:45
  • For those interested, I've described my implementation of ASP.NET Identity in the answer to this question: http://stackoverflow.com/questions/21418902/integrating-asp-net-identity-into-existing-dbcontext. – Gup3rSuR4c Mar 25 '14 at 01:18

1 Answers1

0

Well, I don't have a real solution, but I have one that works. Since the User is not being set during the sign in process, I redirect to another action called DefaultRedirect where I now have full access to the User property and can redirect how I really want from there. It's a dirty cheat that makes things work, but it's not a real solution. I still contend that ASP.NET Identity is somehow not working properly. Anyway, here's my cheat:

    [HttpPost, AllowAnonymous, ValidateAntiForgeryToken]
    public RedirectToRouteResult Default(
        [Bind(Prefix = "Credentials", Include = "Email,Password")] CredentialsInput credentials) {
        if (base.ModelState.IsValid
            && this.AuthenticationService.SignIn(credentials)) {
            return this.RedirectToAction(
                c =>
                    c.DefaultRedirect());
        }

        return this.RedirectToAction(
            c =>
                c.Default());
    }

    [HttpGet]
    public RedirectToRouteResult DefaultRedirect() {
        if (this.User.IsInRole("Technician")) {
            return this.RedirectToAction<TechniciansController>(
                c =>
                    c.Default());
        }

        return this.RedirectToAction(
            c =>
                c.Dashboard());
    }

I'm still looking for a real solution if someone knows about one!

Gup3rSuR4c
  • 9,145
  • 10
  • 68
  • 126