6

Note: Cross-posted to ServerFault, based on comments.

Intro

I need to password protect some actions in my application, such as loading/saving files, clicking check-boxes, etc. This is a standard C# .Net 4.0, WinForms application which will run on Windows 7 in a corporate network.

I was about to roll my own very basic system (read obfuscation with wide open backdoors) with a text file of users/passwords/permissions (hashed and salted) until after some searching I found what looks like a tantalizingly simple approach , but I'm having trouble finding a good tutorial on Roles that isn't about ASP.NET.

Question

So does anyone know of one or more tutorials that show me how to:

  1. Create a Windows User/Group and give that User/Group a Role or Permission.
    • Note that I'm testing this from my company's networked laptop, but will deploy it on the customer's corporate network (Not sure if this is an issue, or how tricky this will get).
  2. Create winforms/console app sample with even just a single method that prints "Hello World" if I'm authenticated or throws an exception if I'm not?
  3. I've never done Network Admin or anything related and I keep reading about Active Directory and Local Users Vs Networked Users... I was hoping for an approach where I could build to an Interface and just ask Windows if the current user has permission ABC and not care too much about how Windows figured that out. Then I can make a concrete implementation for each Local/Network/ActiveDirectory/etc. use case as required (or if required... as I don't even know that right now).

Background

- read if interested, but not required to answer question

Just to make sure I'm going in the right direction here, basically I need/want to test this on my development PC to make sure it's going to have a good end-user experience for my customer. The problem is that currently they run an Auto-login script for each computer that runs my application and there are several different operators that use my application throughout the day. The customer wants password protection on certain features of my app and only provide that to certain operators. I have no problem fitting this in, as I've expected the request for a while, I just haven't ever programmed authentication before.

I think it's worthwhile to convince my customer to give each operator their own network account and assign whatever permissions they want to that operator or group, in case they need to fire somebody, change permissions, etc. It also means I just open several options for them and they can group those permissions however they see fit based on internal corporate policies, which I really shouldn't have to be worried about (but will be if I have to roll my own, as they're IT department knows almost nothing of my application).

From what I can tell it also makes my life a lot easier by not having to deal with hashing passwords and encryption, etc. and just handle which Role is required to click this or that button.

Community
  • 1
  • 1
HodlDwon
  • 1,131
  • 1
  • 13
  • 30
  • Not to be unhelpful, but setting up roles is typically ***not*** a developer's job or responsibility. That's network administration, which would be better at ServerFault. Once a Network Admin has created the roles, THEN a developer can check to see if the user is in the role in code. If you need to work with the admin to define the roles needed for your application, that's a different story... Is this in a work setting, where you have network admins to turn to for assistance? – David Apr 04 '14 at 22:00
  • @DavidStratton I'm a systems integrator for some industrial machines... When I refer to the IT department, it is the customer's IT who have very little to do with me. I'm pretty much the one-stop shop for everything to do with these machines so I need to setup the permissions in code and then tell my customer what is available for them to assign to groups... for example. The customer may even come back to me and **request** that I roll my own password system. So I'm questioning the implementation side more than the network admin side, if that makes sense. – HodlDwon Apr 04 '14 at 22:05
  • Just as clarification for me: Do you want a mechanism, to make certain functions only available for special Windows-Roles or do you want to popup an authentication (password) request popup inside your application, when the user enters special restricted areas? – Andreas H. Apr 06 '14 at 18:34
  • @AndreasH. If I was going to write my own, then I would have a ***login*** button to prompt for ***username/password*** and that would set the available features until you logout, timeout or change accounts. Basically there are some numbers on a screen that only a handful of people should be allowed to change, but pretty much everyone needs to load and use them. I would ***rather*** a prompt instead of having to log out of Windows and re-log in as a different user, but at this stage the prompt seems quite difficult with the framework API. – HodlDwon Apr 06 '14 at 20:30

1 Answers1

10

First of all, you'd have to determine, if you really want a simple role-based-authentication (you may want to read: http://lostechies.com/derickbailey/2011/05/24/dont-do-role-based-authorization-checks-do-activity-based-checks/)

If you're sure it's absolutely sufficient, you're already on the right way with the SO link you provided in your question. It's kind of confusing that there is no support of 'roles' by default in Windows, but there are groups. Groups can be local or remote (e.g. ActiveDirectory), so an admin could assign users to certain groups, that are specific for your application (for an example look here: http://msdn.microsoft.com/en-us/library/ms731200(v=vs.110).aspx)

One key is: You have to prepare your application's central principal, hence fill it with roles, supported for the current user.

Therefore, On the very startup of your application you then check the current active user and set your application wide principal and role(s). This may look like this (just a very simple example):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Security.Principal;
using System.Text;
using System.Threading;

namespace WindowsPrincipalTrial
{
    public class Program
    {
        // you could also move these definitions to a config file
        private static IDictionary<string, string> _groupRoleMappings = new Dictionary<string, string>()
        {
        {"MYAPPUSERGRP", MyRoles.Standard},
        {"MYAPPSUPPORTGRP", MyRoles.Extended},
        {"MYAPPADMINGRP", MyRoles.Admin},
        };

        private static void Main(string[] args)
        {
            var windowsId = WindowsIdentity.GetCurrent();
            if (windowsId != null)
            {
                var allRoleNames = getGroupCorrespondingRoles(windowsId);
                var newPrincipal = new GenericPrincipal(windowsId, allRoleNames);
                Thread.CurrentPrincipal = newPrincipal;
            }
            else
            {
                throw new NotSupportedException("There must be a logged on Windows User.");
            }
        }

        private static string[] getGroupCorrespondingRoles(WindowsIdentity id)
        {
            // you also could do this more elegant with LINQ
            var allMappedRoleNames = new List<string>();

            string roleName;

            foreach (var grp in id.Groups)
            {
                var groupName = grp.Translate(typeof(NTAccount)).Value.ToUpper();
                if (_groupRoleMappings.TryGetValue(groupName, out roleName))
                {
                    allMappedRoleNames.Add(roleName);
                }
            }

            return allMappedRoleNames.ToArray();
        }
    }


    public static class MyRoles
    {
        public const string Standard = "standard_role";
        public const string Extended = "extended_role";
        public const string Admin = "admin_role";
    }
}

Then your Application-Principal is set up. Now you could check access in your code like this:

public void DoSomethingSpecial()
{
    if (Thread.CurrentPrincipal.IsInRole(MyRoles.Extended))
    {
        // do your stuff
    }
    else
    {
        // maybe display an error
    }
}

Or more drastically:

public void DoSomethingCritical()
{
    var adminPermission = new PrincipalPermission(null, MyRoles.Admin);
    adminPermission.Demand();

    // do stuff
}

what is possible even declarative, as known from ASP.NET:

[PrincipalPermission(SecurityAction.Demand, Role=MyRoles.Admin)]
public void DoSomethingMoreCritical()
{
    // do stuff
}

The ugly thing with the latter two examples is, that they throw exceptions, when the right role isn't hit.

So the mapping between roles and groups you have to do quite at the start of your app, according to the systems you want to use (local groups, AD groups, LDAP groups etc.).

If you, however, prefer authentication with actions and roles, after all, have a look at Windows Identity Foundation and Claims Based Authorization! There are already some ready-to-use frameworks out there (e.g. https://github.com/thinktecture/Thinktecture.IdentityModel).

UPDATE:

When it comes to activity based and thereby claims based authorization, I will try in short, how you could achieve it, by using Thinktecture's IdentityModel.

Generally that approach still uses roles internally, but has a kind of translation layer in between. Thinktecture already encapsulates many things needed. Authorization checks in code are then done via claim permissions. They are technically kind of request for an access to a certain resource. For the sake of simplicity I limit my example for actions only, by using one single default resource (since ClaimPermission doesn't accept an empty resource). If you want to use action@resource pairs, you'd have to modify the code respectively.

At first you need a ClaimsAuthorizationManager

    public class MyClaimsAuthorizationManager : ClaimsAuthorizationManager
    {
        private IActivityRoleMapper _actionToRolesMapper;

        public MyClaimsAuthorizationManager(IActivityRoleMapper mapper)
        {
            _actionToRolesMapper = mapper;
        }

        public override bool CheckAccess(AuthorizationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            try
            {
                var action = getActionNameFromAuthorizationContext(context);

                var sufficientRoles = _actionToRolesMapper.GetRolesForAction(action)
                    .Select(roleName => roleName.ToUpper());

                var principal = context.Principal;

                return CheckAccessInternal(sufficientRoles, principal);
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        protected virtual bool CheckAccessInternal(IEnumerable<string> roleNamesInUpperCase, IClaimsPrincipal principal)
        {
            var result = principal.Identities.Any(identity =>
                    identity.Claims
                        .Where(claim => claim.ClaimType.Equals(identity.RoleClaimType))
                        .Select(roleClaim => roleClaim.Value.ToUpper())
                        .Any(roleName => roleNamesInUpperCase.Contains(roleName)));

            return result;

        }

        // I'm ignoring resources here, modify this, if you need'em
        private string getActionNameFromAuthorizationContext(AuthorizationContext context)
        {
            return context.Action
                .Where(claim => claim.ClaimType.Equals(ClaimPermission.ActionType))
                .Select(claim => claim.Value)
                .FirstOrDefault();
        }
    }

As you may have guessed, IActivityRoleMapper is an interface for a class, that returns the names of all roles, that include permission for a given action. This class is very individual and I guess you'll find your way implementing it, because it's not the point here. You could do it by hardcoding, loading from xml or from a database. Also you would have to change/extend it, if you wanted to you action@resource pairs for permission requests.

Then you'd have to change the code in main() method to:

using Thinktecture.IdentityModel;
using Thinktecture.IdentityModel.Claims;
using Microsoft.IdentityModel.Web;



private static void Main(string[] args)

    {
        var windowsId = WindowsIdentity.GetCurrent();
        if (windowsId != null)
        {
            var rolesAsClaims = getGroupCorrespondingRoles(windowsId)
                .Select(role => new Claim(ClaimTypes.Role, role))
                .ToList();

            // just if you want, remember the username
            rolesAsClaims.Add(new Claim(ClaimTypes.Name, windowsId.Name));

            var newId = new ClaimsIdentity(rolesAsClaims, null, ClaimTypes.Name, ClaimTypes.Role);

            var newPrincipal = new ClaimsPrincipal(new ClaimsIdentity[] { newId });
            AppDomain.CurrentDomain.SetThreadPrincipal(newPrincipal);

            var roleMapper = new ActivityRoleMapper(); // you have to implement

            // register your own authorization manager, so IdentityModel will use it per default
            FederatedAuthentication.ServiceConfiguration.ClaimsAuthorizationManager = new MyClaimsAuthorizationManager(roleMapper);
        }
        else
        {
            throw new NotSupportedException("There must be a logged on Windows User.");
        }
    }

Finally you can check access this way:

    public const string EmptyResource = "myapplication";

    public void DoSomethingRestricted()
    {
        if (!ClaimPermission.CheckAccess("something_restricted", EmptyResource))
        {
            // error here
        }
        else
        {
            // do your really phat stuff here
        }
    }

Or again, with exceptions:

private static ClaimPermission RestrictedActionPermission = new ClaimPermission(EmptyResource, "something_restricted");

public void DoSomethingRestrictedDemand()
{
    RestrictedActionPermission.Demand();

    // play up, from here!
}

Declarative:

    [ClaimPermission(SecurityAction.Demand, Operation = "something_restricted", Resource = EmptyResource)]
    public void DoSomethingRestrictedDemand2()
    {
        // dostuff
    }

Hope this helps.

Andreas H.
  • 766
  • 9
  • 17
  • I greatly appreciate the detailed answer. I was already thinking of more of an Activity style approach than strictly Roles, so the article you linked helped clarify some thoughts I had. One question I still have though is do I have to do anything different in an Active Directory network vs some other Windows based network or will the `WindowsIdentity.GetCurrent()` always just work? I really am looking for a simple "***pass it off to the OS***" approach. – HodlDwon Apr 06 '14 at 23:34
  • Oh, I also get the impression that `Thread.CurrentPrincipal.IsInRole(...)` will get complicated in my multi-threaded environment. Does it play nice with `Task`s or must each task be explicitly assigned any permissions it needs? (I'm thinking app wide authorization vs thread authorization). – HodlDwon Apr 06 '14 at 23:42
  • 3
    @Josh W: You could set your principal with AppDomain.CurrentDomain.SetThreadPrincipal(newPrincipal), so your principal will be used for every newly created thread from then on and you don't have to worry, just use Thread.CurrentPrincipal.IsInRole(). – Andreas H. Apr 07 '14 at 05:59
  • @JoshW: The WindowsIdentity should include AD-Groups, too, but you have to make sure, that the group names you're mapping are correctly prefixed (e.g. "AD\MYAPPDEFAULTGRP"). When it comes to different network environments, I do not know, if their groups are included. Maybe you would have to check for special networks and then handle them differently according to their API. – Andreas H. Apr 07 '14 at 06:08
  • I updated my answer above by a simple activity based approach. – Andreas H. Apr 07 '14 at 09:01
  • Great answer, thank you. I'll probably go with some mix of your first and second approach (.Net 4.0 and not a whole lot of time to learn Thinktekture API). I think for now I can shoehorn AD user group names into an activity type semantics. I'll dig into claims at a later date. Thanks again. – HodlDwon Apr 07 '14 at 15:36
  • @AndreasH. How do I prevent the user from running this in a debugger and stepping over the `if(CheckPermissions())` line? – Dan Bechard Apr 12 '16 at 13:21