ServiceStack.NET Windows Authentication (NTLM) in ASP.NET MVC

MVCible picture MVCible · Mar 6, 2013 · Viewed 8.6k times · Source

How to implement Windows Authentication in a ServiceStack project build on ASP.NET MVC4?

I started with a global Request-Filter added in the AppHost:

private void ConfigureAuth(Funq.Container container)
{
    this.RequestFilters.Add((httpReq, httpResp, requestDto) =>
    {
        var user = HttpContext.Current.User.Identity;
        if (!user.IsAuthenticated ||
            !user.Name.Contains(_myTestUser)) //todo: check username here in database (custom logic) if it has access to the application
            httpResp.ReturnAuthRequired();
    });
}

This opens up a login dialog, which if entered correctly (username exists and valid password is entered and also the myTestUser is set to this), results in a successful response. If anything is wrong, the login dialog is shown again. -- Thats sounds ok to me. But after retyping the correct user in that second login window, it stops working. The dialog opens again, if like its again incorrect. No breakpoint is hit inside the filter function.

Any idea what might cause this?

Thats what i added in the web.config:

<authentication mode="Windows"/>
<authorization>
  <deny users="?" /> <!--only allow authenticated users-->
</authorization>

I want to completely lock up the website and enable access to specified windows users in the database only with their specific permissions (roles). I need to implement custom logic to access the "list of users and roles". Maybe there is an other way to do this in MVC4/ ASP.NET?

Answer

pjsvis picture pjsvis · Mar 27, 2013

ServiceStack Custom Authentication for Windows Intranets

I have been batting my head against this all day and have come up with the following.

First the use case:

You are on a corporate intranet using Windows Authentication. You set up authentication mode="Windows" in your web.config and that's it!

Your strategy is this:

  1. You dont' know who the user is because they are not in your table of users or ActiveDirectory group or whatever. In this case you give them the role of "guest" and trim the UI accordingly. Maybe give them an email link to request access.

  2. You have the user in your list of users but they have not been assigned a role. So give them the role of "user" and trim the UI as above. Maybe they can see their stuff but nothing else.

  3. The user is in your list and has been assigned a role. Initially you will assign the role by manually updating the UserAuth table in the database. Eventually you will have a service that will do this for authorised users.

So let's get to the code.

Server Side

In ServiceStack Service layer we create a Custom Credentials Authorisation Provider as per https://github.com/ServiceStack/ServiceStack/wiki/Authentication-and-authorization

      public class CustomCredentialsAuthProvider : CredentialsAuthProvider
        {
            public override bool TryAuthenticate(IServiceBase authService, string userName, string password)
            {
                //NOTE: We always authenticate because we are always a Windows user! 
                // Yeah, it's an intranet  
                return true;
            }

            public override void OnAuthenticated(IServiceBase authService, IAuthSession session, IOAuthTokens tokens, Dictionary<string, string> authInfo)
            {

                // Here is why we set windows authentication in web.config
                var userName = HttpContext.Current.User.Identity.Name;

                //  Strip off the domain
                userName = userName.Split('\\')[1].ToLower();

                // Now we call our custom method to figure out what to do with this user
                var userAuth = SetUserAuth(userName);

                // Patch up our session with what we decided
                session.UserName = userName;
                session.Roles = userAuth.Roles;            

                // And save the session so that it will be cached by ServiceStack 
                authService.SaveSession(session, SessionExpiry);
            }

        }

And here is our custom method:

     private UserAuth SetUserAuth(string userName)
            {
                // NOTE: We need a link to the database table containing our user details
                string connStr = ConfigurationManager.ConnectionStrings["YOURCONNSTRNAME"].ConnectionString;
                var connectionFactory = new OrmLiteConnectionFactory(connStr, SqlServerDialect.Provider);

                // Create an Auth Repository
                var userRep = new OrmLiteAuthRepository(connectionFactory);

                // Password not required. 
                const string password = "NotRequired";

                // Do we already have the user? IE In our Auth Repository
                UserAuth userAuth = userRep.GetUserAuthByUserName(userName);

                if (userAuth == null ){ //then we don't have them}

                // If we don't then give them the role of guest
                userAuth.Roles.Clear();
                userAuth.Roles.Add("guest")

                // NOTE: we are only allowing a single role here               

                // If we do then give them the role of user
                // If they are one of our team then our administrator have already given them a role via the setRoles removeRoles api in ServiceStack
               ...

                // Now we re-authenticate out user
                // NB We need userAuthEx to avoid clobbering our userAuth with the out param
                // Don't you just hate out params?

                // And we re-authenticate our reconstructed user
                UserAuth userAuthEx;
                var isAuth = userRep.TryAuthenticate(userName, password, out userAuthEx);
                return userAuth;
            }

In appHost Configure add the following ResponseFilters at the end of the function

    ResponseFilters.Add((request, response, arg3) => response.AddHeader("X-Role",request.GetSession(false).Roles[0]));
    ResponseFilters.Add((request, response, arg3) => response.AddHeader("X-AccountName", request.GetSession(false).UserName));

This sends some additional headers down to the client so that we can trim the UI as per the user's role.

Client Side

On the client side when we make out first request to the server we POST a UserName and Password as required by Custom Authentication. Both are set to "NotRequired" as we will know who the user is on the server side via HttpContext.Current.User.Identity.Name.

The following uses AngularJS for AJAX comms.

    app.run(function($templateCache, $http, $rootScope) {

        // Authenticate and get X-Role and X-AccountName from the response headers and put it in $rootScope.role

        // RemeberMe=true means that the session will be cached 
        var data={"UserName" : "NotRequired", "Password" : "NotRequired", "RememberMe": true };

        $http({ method : 'POST', url : '/json/reply/Auth', data : data }).
            success(function (data, status, headers, config) {
            // We stash this in $rootScope for later use!
                $rootScope.role = headers('X-Role');
                $rootScope.accountName = headers('X-AccountName');
                console.log($rootScope.role);
                console.log($rootScope.role);
            }).
            error(function (data, status, headers, config) {
                // NB we should never get here because we always authenticate
                toastr.error('Not Authenticated\n' + status, 'Error');
            });
    };