Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC Authenticate before controller instantiated

I have a controller where I am injecting a service interface into the constructor. The service has interfaces injected into its constructor as well. The IoC container (Unity) needs to use information about the user when constructing one of the classes that it returns for a given interface.

What is happening, is that the controller is being instantiated before the [Authorize] attribute is evaluated and the user is authenticated. This forces Unity to perform the dependency injection and use information about the user before they have logged in. None of this was a problem when we were using integrated windows authentication, but now we are using OpenID Connect to Azure AD and the user info isn't there until they log in (which happens AFTER the controller is instatiated).

I heard (in other posts) that there is a way to configure my owin startup class to move authentication earlier in the process, but I cannot find any examples on how to do this. I need the authentication to happen before the controller is instantiated.

Here is a simplified example of what I have...

Controller:

[Authorize]
public class MyController : Controller
{
    private readonly IMyService myService;

    public MyController(IMyService myService)
    {
        this.myService = myService;
    }

    // ...
}

Unity Configuration:

public class UnityBootstrap : IUnityBootstrap
{
    public IUnityContainer Configure(IUnityContainer container)
    {
        // ...

        return container
            .RegisterType<ISomeClass, SomeClass>()
            .RegisterType<IMyService>(new InjectionFactory(c =>
            {
                // gather info about the user here
                // e.g.
                var currentUser = c.Resolve<IPrincipal>();
                var staff = c.Resolve<IStaffRepository>().GetBySamAccountName(currentUser.Identity.Name);
                return new MyService(staff);
            }));
    }
}

OWIN Startup (Startup.Auth.cs):

public void ConfigureAuth(IAppBuilder app)
{
    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

    app.UseCookieAuthentication(new CookieAuthenticationOptions());

    app.UseOpenIdConnectAuthentication(
        new OpenIdConnectAuthenticationOptions
        {
            ClientId = this.clientID,
            Authority = this.authority,
            PostLogoutRedirectUri = this.postLogoutRedirectUri,
            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                RedirectToIdentityProvider = context =>
                {
                    context.ProtocolMessage.DomainHint = this.domainHint;
                    return Task.FromResult(0);
                },
                AuthorizationCodeReceived = context =>
                {
                    var code = context.Code;

                    var credential = new ClientCredential(this.clientID, this.appKey.Key);
                    var userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
                    var authContext = new AuthenticationContext(this.authority, new NaiveSessionCache(userObjectID));
                    var result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, this.graphUrl);
                    AzureAdGraphAuthenticationHelper.Token = result.AccessToken;
                    return Task.FromResult(0);
                }
            }
        });
}
like image 882
M.Ob Avatar asked Jan 29 '15 20:01

M.Ob


1 Answers

After failing to find anything specifically addressing my issue online, I decided to dig into the ASP.NET MVC 5 application lifecycle. I found that I could create a custom IControllerFactory (or inherit from DefaultControllerFactory) where I could define the CreateController method.

During CreateController, I check to see if the user is authenticated. If they are, I simply let the DefaultControllerFactory create the controller as it normally would.

If the user is not authenticated, I create my (very) simple "Auth" controller instead of the controller requested (the one with the many layers of dependencies) with the RequestContext left unchanged.

The Auth controller will be instantiated with no problem, as it has no dependencies. Note: no action is ever executed on the Auth controller. Once the Auth controller is created, the global AuthorizeAttribute kicks in and the user is directed to authenticate (via OpenID Connect to Azure AD and ADFS).

After signing in, they are redirected back to my app with the original RequestContext still in tact. The CusomControllerFactory sees the user as authenticated and the requested controller is created.

This method works great for me as my controllers have a large dependency chain being injected (i.e. Controller depends on ISomeService which depends on many ISomeRepository, ISomeHelper, ISomethingEles...) and none of the dependencies are resolved until the user is logged in.

I am still definitely open to hearing other (more elegant) ideas on how to achieve what I was asking in my original question.


CustomControllerFactory.cs

public class CustomControllerFactory : DefaultControllerFactory
{
    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var user = HttpContext.Current.User;
        if (user.Identity.IsAuthenticated)
        {
            return base.CreateController(requestContext, controllerName);
        }

        var routeValues = requestContext.RouteData.Values;
        routeValues["action"] = "PreAuth";
        return base.CreateController(requestContext, "Auth");
    }
}

Global.asax.cs

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        // ...
        ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
    }

    // ...
}

AuthController.cs

public class AuthController : Controller
{
    public ActionResult PreAuth()
    {
        return null;
    }
}
like image 131
M.Ob Avatar answered Sep 23 '22 01:09

M.Ob