While working on an Umbraco project, I needed to add a feature, which allowed editors to log in the backoffice using accounts from Active Directory. The requirement was to allow only editors who belong to certain groups in the AD.

The following post from our.umbraco.org contains an example on how to successfully connect to Active Directory, but this standard method does not allow you to specify a group of users: https://our.umbraco.org/wiki/how-tos/membership-providers/active-directory-membership-provider. It  is important to execute the steps at the bottom of the post when you connect Umbraco to your AD.

Standard ASP.NET Implementation

My solutions is based on the following post about the standard ASP.NET integration with Active Directory: https://stackoverflow.com/questions/726837/user-group-and-role-management-in-net-with-active-directory

This is a very good post, but for some reason in Umbraco, the code in ActiveDirectoryRoleProvider never got hit so I moved the functionality to the ActiveDirectoryMembershipProvider.

Solution

ActiveDirectoryMembershipProvider

Following is the code for the ActiveDirectoryMembershipProvider:

public class ActiveDirectoryMembershipProvider : System.Web.Security.ActiveDirectoryMembershipProvider
{
    private string _domain;
    private IEnumerable<string> _allowedGroups;

    /// <summary>
    /// Initialize ActiveDirectoryMembershipProvider with config values.
    /// </summary>
    public override void Initialize(string name, NameValueCollection config)
    {
        if (config == null)
            throw new ArgumentNullException("config");

        _domain = ReadConfigMandatory(config, "domain");
        _allowedGroups = ReadConfig(config, "allowedGroups").Split(',').Select(group => group.Trim());

        // Initialize the abstract base class.
        base.Initialize(name, config);
    }

    /// <summary>
    /// Validate the user.
    /// </summary>
    /// <returns></returns>
    public override bool ValidateUser(string username, string password)
    {
        bool isValid = false;
        try
        {
            using (PrincipalContext context = new PrincipalContext(ContextType.Domain, _domain, username, password))
            {
                if (context.ValidateCredentials(username, password))
                {
                    UserPrincipal user = UserPrincipal.FindByIdentity(context, username);
                    if (user != null)
                    {
                        // Get all direct groups, which the user belongs to.
                        List<string> userGroups = user.GetGroups().Select(g => g.Name).ToList();
                        // Prepare a list with all user groups.
                        List<string> allUserGroups = new List<string>(userGroups);
                        foreach (string userGroup in userGroups)
                        {
                            // Discover all groups, to which the user's groups belong to.
                            RecurseGroup(context, userGroup, allUserGroups);
                        }

                        isValid = allUserGroups.Join(_allowedGroups, r => r, g => g, (r, g) => r).Any();
                    }
                }
            }
        }
        catch
        {
            // TODO: log exception
        }

        return isValid;
    }

    /// <summary>
    /// Reads settings from the config and removes them.
    /// </summary>
    /// <param name="config">The config settings.</param>
    /// <param name="key">The setting key.</param>
    /// <returns>The setting value.</returns>
    private string ReadConfig(NameValueCollection config, string key)
    {
        if (config.AllKeys.Any(k => k == key))
        {
            string value = config[key];
            config.Remove(key);
            return value;
        }
        return string.Empty;
    }

    /// <summary>
    /// Reads settings from the config and removes them. Throws an exception if the setting does not exits.
    /// </summary>
    /// <param name="config">The config settings.</param>
    /// <param name="key">The setting key.</param>
    /// <returns>The setting value.</returns>
    private string ReadConfigMandatory(NameValueCollection config, string key)
    {
        string value = ReadConfig(config, key);
        if (string.IsNullOrEmpty(value))
        {
            throw new ProviderException("Configuration value required for key: " + key);
        }

        return value;
    }

    /// <summary>
    /// Discovers the groups for a group.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="group"></param>
    /// <param name="groups"></param>
    private void RecurseGroup(PrincipalContext context, string group, List<string> groups)
    {
        var principal = GroupPrincipal.FindByIdentity(context, IdentityType.SamAccountName, group);
        if (principal == null)
        {
            return;
        }

        IList<string> newGroups = principal.GetGroups().Select(g = > g.Name)
        // Remove all existing groups.
        .Except(groups).ToList();

        groups.AddRange(newGroups);

        foreach (string newGroup in newGroups)
        {
            RecurseGroup(context, newGroup, groups);
        }
    }
}

Edit: The code between lines 35 and 44 was updated with a bug fix.

Note that you will need to add references to System.DirectoryServices and System.DirectoryServices.AccountManagement.

web.config

Modify your web.config with the following values:

<connectionStrings>
    <add name = "YourADConnectionString" connectionString="LDAP://yourdomain.com/DC=yourdomain,DC=com" />
</connectionStrings>
<system.web>
    <membership defaultProvider = "YourADMembershipProvider">
        <add name = "YourADMembershipProvider"
        type="YourNamespace.ActiveDirectoryMembershipProvider"
        connectionStringName="YourADConnectionString"
        connectionUsername="yourdomain\authorizeduser"
        connectionPassword="yourpassword"
        attributeMapUsername="sAMAccountName"
        enablePasswordReset="false"
        domain="yourdomain.com"
        allowedGroups="Umbraco Users,Other Group" />
    </membership>
</system.web>

umbracoSettings.config

Also modify your umbracoSettings.config with the following values:

<settings>
    <providers>
        <users>
            <DefaultBackofficeProvider>YourADMembershipProvider</DefaultBackofficeProvider>
        </users>
    </providers>
</settings>

29 Comments

biustonosz · June 28, 2013 at 12:28

Excellent post! We are linking to this great
content on our site. Keep up the good writing.

vassvdm · June 28, 2013 at 12:30

Hey Nikolay. Interesting stuff! I’m working on the new online workplace Cresters (www.cresters.com) and we’re currently looking for freelancers skilled in .NET and Umbraco. Would you like to have a look and tell me what you think? It would be great to have your feedback. Anyway, have a nice day! 🙂
Vassili

    Nikolay Arhangelov · June 30, 2013 at 21:41

    Hello Vassili,

    Thank you for the opportunity! I appreciate that you contacted me 🙂 I am currently working on a large project for my employer and I am tied to them for the next year.

    Best regards,
    Nikolay

      vassvdm · June 30, 2013 at 21:52

      Sure, no problem. If ever you wanted to work on your spare time for added income you might want to create an account – it’s a one-click affair if you have a LinkedIn account. And there is already a relevant gig that’s been posted for your skills: https://www.cresters.com/task/51cd5681e4b0d949f48fb68e. Have a great day!
      Vassili

kabaretki · June 29, 2013 at 00:09

Very nice article, just what I was looking for.

Scott Bentley · September 11, 2013 at 19:59

Great article. My only issue with this, and just the implementation listed on the umbraco forums, I dont want a username and password in my web.config. I dont know enough about these membership providers to know if there’s a way to get those two bits of information from somewhere else, i.e. the registry.

    Nikolay Arhangelov · September 27, 2013 at 15:21

    Hello,
    I haven’t tried this, but I believe that you should be able to encrypt this section of the config or store the credentials in the registry.

Connie DeCinko · September 24, 2013 at 23:13

Would your solution only be applicable to the back office or could you also use this to manage users? I need a solution not only to manage what staff can edit content, I need to block access to certain Intranet web pages as well. The Umbraco rolls work fine, just wondering if I want to manage rolls in AD or in Umbraco.

    Nikolay Arhangelov · September 24, 2013 at 23:20

    Hello,
    The implementation in the post will only grant access to the backoffice. I haven’t thought of this scenario – it is interesting and I will further investigate what needs to be done so that AD can determine the role of the user inside the backoffice.

Connie DeCinko · October 1, 2013 at 20:41

I finally got this setup to the point where it no longer throws an error however I still cannot login to Umbraco. Is there a log I can check for errors? Any common issues I can look for? Is this supposed to auto log me in or will users still be required to enter their username and password to access the Umbraco backoffice?

    Nikolay Arhangelov · October 1, 2013 at 20:49

    In order for this to work, the AD users need to be added to a group – initially my users were added to an AD container, not group. If you can debug it, see if principal.GetGroups() returns any groups for the user.

      Connie DeCinko · October 2, 2013 at 02:20

      I don’t see a way to debug unless I dump the groups to a log file. Is PrincipalContext supposed to be passed the credentials for the Umbraco user or the credentials to connect to AD?

Connie DeCinko · October 1, 2013 at 20:49

Sorry, another question. Does your method require the user already have an account created in Umbraco? Is the point of using AD just to centralize their password and manage if they are part of the Umbraco authors group?

    Nikolay Arhangelov · October 1, 2013 at 20:58

    With AD as a provider, Umbraco will no longer store passwords. When an authorized user logs in for the first time, Umbraco will automatically create an account for him in its database, but it won’t store a password – just username and email.
    If the user changes his password in AD, he will be able to log in Umbraco with the new password.

    Using AD will allow you to grant access only to certain AD users. It will also provide a good experience – the users won’t have to create new accounts and remember passwords.

      Connie DeCinko · October 2, 2013 at 02:21

      I changed to the standard AD provider and that worked. User did not exist in Umbraco prior to login and their account was created, so I know that works. However when I use your code above I get an error about not being able to connect the the LDAP server.

Nikolay Arhangelov · October 2, 2013 at 10:15

The code is derived from the standard AD provider and only overrides the user validation – the connection to AD is from the standard provider.
I will see if I can reproduce this bug.

    Connie DeCinko · October 2, 2013 at 19:19

    I seem to have located the issue, just not the resolution… yet. The ADC we are trying to hit is not in the same domain as the web server. I think we need to use ContextType.Machine but so far, I’m getting General access denied error. I can see in netstat that the web server is talking to the ADC.

      Connie DeCinko · October 2, 2013 at 20:02

      Ok, problem now seems to be I am unable to get the user groups: Information about the domain could not be retrieved (1355). If I comment out that section, no error, I just get the red jiggles.

Nikolay Arhangelov · October 2, 2013 at 21:07

It seems that error 1355 means ‘The specified domain either does not exist or could not be contacted.’ Here is a forum thread about the problem – I hope it helps:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/219c4b4b-b43a-4dbc-9e3c-a1135879c5f9/information-about-the-domain-could-not-be-retrieved-1355

Connie DeCinko · October 2, 2013 at 21:21

Thanks but those didn’t help. The domain exists and can be contacted. Problem now seems to be we can’t access the groups.

hardywang · April 25, 2014 at 21:17

Very interesting. There is one question though, if the user in the configured AD group logs in for the first time, what permission does the user get? Admin, editor, writer, translator or nothing?

    Nikolay Arhangelov · April 25, 2014 at 22:37

    If a user logs for the first time, he becomes a writer. I think that he also sees only the Content section.

      Wesley Herpoelaert (@w_herpoelaert) · May 6, 2014 at 16:04

      Is it possible to change that behaviour, that the user also sees the Media section when he logs in for the first time?

        Nikolay Arhangelov · May 7, 2014 at 10:32

        Hello,

        The following application event handler should add the Media section.

        public class ReaderUserHandling : ApplicationEventHandler
        {
        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
        // Make all new users readers.
        User.New += OnNewUser;
        }

        private void OnNewUser(User newUser, EventArgs e)
        {
        // Add the media application.
        newUser.addApplication(Constants.Applications.Media);
        newUser.Save();
        }
        }

Zaheed · February 15, 2016 at 16:14

Hi Nikolay Arhangelov,
Does your implementation works for the Umbraco version 7.3. As i have Implemented as per the above code and it throws an exception Unable to cast object of type ‘System.Web.Security.ActiveDirectoryMembershipProvider’ to type ‘Umbraco.Core.Security.UmbracoMembershipProviderBase’. Can you please let know what changes should I make in the code

    Nikolay Arhangelov · February 15, 2016 at 17:53

    Hello Zaheed,

    Unfortunately I haven’t had the chance to try this out in Umbraco 7.

3ijt · August 18, 2017 at 13:26

Is there a possibility to make the user automatically logged in?

    Nikolay Arhangelov · August 18, 2017 at 13:49

    I think you should be able to by using Windows authentication in you web.config. You’ll need to make sure that the authentication only works for the admin panel and not the public pages.

      3ijt · August 21, 2017 at 12:55

      Well, right now I’m using “Active Directory Providers” for auto authentication via Active Directory for the public website, and “Umbraco Back Office Active Directory ASP.Net Provider” for the backoffice (with role support) all set up to use Windows authentication, and I have no issues with auto loging in on the public pages, but I do on the backoffice. I still need to fill in credentials every time 🙁

Leave a Reply

Your email address will not be published. Required fields are marked *