NuGet with Active Directory Support

Published on Author Michael

In a previous article I discussed how to host a private NuGet repository.  If you aren’t familiar with NuGet then please refer to that article.  If you’re hosting a private gallery then chances are you’re on a network (probably an Active Directory one).  One downside to hosting a private NuGet gallery is that it is tied to Forms authentication.  For a public repository this makes sense but for a private one it would be better to have NuGet use Windows authentication.  This article will discuss the changes that have to be made to the NuGet source to support Windows authentication.  The changes are scattered but minor. 

NuGet Authentication

Official NuGet uses Forms authentication.  When a user is browsing the site or downloading packages then they have not logged in yet.  When a user attempts to do something like manage a package or upload one then NuGet prompts for login.  It does this using the standard Authorize attribute on the appropriate controller actions.  If the user does not have an account yet then they are prompted to register.  The registration process associates the user name, password and email address.  If configured then the user must confirm their email address before their account is recognized.  If the user owns packages then emails sent from NuGet will go to their registered email address.

NuGet defines a single role – Admin.  A user is either an admin or they are not.  There is no UI for assigning roles.  Instead a database call needs to occur.  A normal user can manage their own packages and send emails to other owners.  An admin can manage any packages and assign ownership.

NuGet View Modes

Out of the box it would seem that adding Windows authentication to NuGet would be a simple matter of changing the authentication mode but unfortunately it isn’t that easy.  NuGet can be accessed several different ways.  Some support Windows auth and some don’t.

  • Website viewing – In this mode a user should be able to view the packages and even download them without having to be authenticated.  In terms of Windows auth any user should probably have permissions.
  • Website uploading – Only users who are authenticated should be able to upload packages. 
  • Visual Studio – VS will connect to NuGet to get the list of packages and to download them using the web API.  This shouldn’t require any special privileges.  More importantly VS will not respond to a challenge/response from the server so Windows auth cannot be used.

Because of the different ways of accessing NuGet it seems that the best approach will be to allow anybody to access the site (assuming they have the right Windows group membership).  The Admin role can remain and be used as needed.  A new role, Authors, is needed to control who can upload packages.  This role is only used when accessing the website for uploads.

Before going any further you need to get the NuGet source and ensure it compiles correctly.  I walked through that process in the previous article.  We need to make some changes to the NuGet files and we need to add some additional files.  I’ll walk through the process step by step.

Preparing NuGet

Before continuing further be sure that Windows authentication has been installed for IIS.  Also ensure that the app pool that is hosting NuGet has read/write permissions to the App_Data directory.  All other IIS changes will be handled by the web.config file.  If you are using a version of IIS prior to Server 2008 then you might need to make some of these changes to IIS directly.

We will be adding new files to NuGet and making changes to existing ones so we want to try to keep them separated.  In the NuGet website add a new folder to store the new files (i.e. Custom).  Whenever we add new files they will go here.

Replacing User Service

NuGet uses IUserService to create and manage users.  It is clear that IUserService was written based upon a Forms auth approach because it doesn’t really encapsulate anything other than Forms auth.  For the most part we just need to replace a few methods that don’t make sense in an AD environment.  To do that we’ll create a new type called ADUserService and have NuGet use it instead.  Because we aren’t providing a full implementation we’ll derive from the existing type.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Security;

namespace NuGetGallery
{
    public class ADUserService : UserService
    {
        public ADUserService ( GallerySetting settings,
                               ICryptographyService cryptoService,
                               IEntityRepository<User> userRepository )
            : base(settings, cryptoService, userRepository)
        {
        }
    }    
}

When a user registers with NuGet they must enter their user name, password and email.  Once the user has entered the information it is stored in the database by calling Create.  The user is associated with packages through the user table in the database.  Irrevelant of authentication we need to ensure that the entry is created.  For AD we don’t need to do anything different except for the confirmation email and password.  By default a confirmation email is sent that the user must respond to.  For AD we don’t need to send a confirmation email.  It is a setting in NuGet to disable confirmation emails but we’ll go ahead and ensure that the email is confirmed when the user is created.

public override User Create ( string username, string password, string emailAddress )
{
    var user = base.Create(username, password, emailAddress);

    //Confirm the email address
    ConfirmEmailAddress(user, user.EmailConfirmationToken);

    return user;
}

Whenever NuGet needs user information it will find the user by calling one of several find methods.  A couple of these require a password.  Since we’re using AD we don’t need the password (in fact we won’t have it).  When we create the user we’ll use a dummy password.  When searching for a user we will use only the user’s name and ignore any password.

public override User FindByUsernameAndPassword ( string username, string password )
{
    //Just search by user name
    return base.FindByUsername(username);
}

public override User FindByUsernameOrEmailAddressAndPassword ( string usernameOrEmail, string password )
{
    //Ignore the password
    return base.FindByUsername(usernameOrEmail) ?? base.FindByUsername(usernameOrEmail);
}

That completes the change for the user service.  Now we just need to register it by modifying ContainerBindings.Load (App_Start\ContainerBindings.cs).

//MODIFIED: Use ADUserService
Bind<IUserService>()
    .To<ADUserService>()
    .InRequestScope();

 

Updating the User View

The view UserDisplay.cshtml is responsible for displaying the log on, register and log off buttons.  None of these make sense with Windows auth so modify the view to remove them.  I went ahead and displayed the full domain name of the user.  If you wanted to get fancy you could either display only the user name or even query AD to get the user’s display name.

<div class=”user-display”>
   <span class=”welcome”>@User.Identity.Name</span>
</div>

 

Adding Authors Role

By default a user going to NuGet won’t require authentication so the user service won’t be called.  However when a user does anything that requires authentication such as trying to upload a package they will get redirected to the login page.  This happens because NuGet uses the Authorize attribute on the controller action.  Since we’ll be using Windows auth the user will already be authorized so we don’t need to check for authentication so we could remove the attribute.  But at some point a user has to be added to the NuGet database in order to upload packages.  We could do that when the user comes to the site but presumably most users don’t need an account.  Instead it is probably better to only create the NuGet account when the user tries to do something that requires one (i.e. an author action).  So instead of removing the authorization attribute we’ll create a new action filter attribute that verifies the user is in the Authors role.  As part of this check the filter will ensure that the user has an account in the NuGet database.  Add a new action filter called MustBeAuthorAttribute.

public class MustBeAuthorAttribute : AuthorizeAttribute
{
    public MustBeAuthorAttribute ()
    {
        this.Roles = “Authors”;
    }

    public string ViewUrl
    {
        get { return “~/Users/Account/AccessDenied”; }
    }

    public override void OnAuthorization ( AuthorizationContext filterContext )
    {
        base.OnAuthorization(filterContext);

        if (filterContext.Result is HttpUnauthorizedResult)
        {
            filterContext.Result = new RedirectResult(ViewUrl);
            return;
        };

        EnsureUserIsRegistered(filterContext.HttpContext.User.Identity.Name);
    }

    private void EnsureUserIsRegistered ( string username )
    {
        var svc = GetUserService();
        var user = svc.FindByUsername(username);
        if (user == null)
        {
            //Get the user name without the domain
            var member = Membership.GetUser(username.Split(‘\\’).Last());

            //Create the user in NuGet
            svc.Create(username, “”, member.Email);
        };
    }

    private IUserService GetUserService()
    {
        return (IUserService)Container.Kernel.GetService(typeof(IUserService));
    }
}

In order to authenticate the user must be in the Authors role (we’ll set this up later).  If the user is authenticated then we confirm they have a user account with NuGet by looking them up.  If we don’t find them then we call the Membership API to get their email address (we’ll see how this works later) and then creating their user account.  Notice the password is empty because we don’t need it.  The final step is to replace all instances of [Authorize] with the new attribute.

If the user is not authenticated then they are redirected to a new access denied page.

@model SignInRequest
@
{
   ViewBag.Title = “Access Denied”;
}

<h1>Access Denied</h1>

<p> You have insufficient privileges to acces this page.</p>

 

public partial class AuthenticationController : Controller
{
    public virtual ActionResult AccessDenied ()
    {
        return View();
    }
    …
}

 

Switching to AD Authentication

It’s time to modify the config file to use AD authentication instead of Forms authentication.  .NET already ships with a provider that will use AD and implement the functionality needed by the Membership API.  All we need to do is configure it.  Replace the existing authentication element with this.

<authentication mode=”Windows” />

Add an authorization element that allows all users access to the site.  You could technically limit access to specific users (such as developers) but that might cause problems with Visual Studio.  Also note that the config file already contains an element under a location element.  Do not change that element!!

<authorization>
   <allow users=”*” />
</authorization>

Add the necessary role and membership providers to use AD authentication.

<roleManager enabled=”true” cacheRolesInCookie=”false” defaultProvider=”RoleManagerAzManProvider“>
    <providers>
        <clear />
        <add name=”RoleManagerAzManProvider” type=”System.Web.Security.AuthorizationStoreRoleProvider,System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
                connectionStringName=”LocalPolicyStore” applicationName=”NuGet” />
    </providers>
</roleManager>
<membership defaultProvider=”ActiveDirectoryProvider“>
    <providers>
        <add name=”ActiveDirectoryProvider” type=”System.Web.Security.ActiveDirectoryMembershipProvider,System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
                connectionStringName=”ActiveDirectoryConnection” connectionProtection=”Secure” enablePasswordReset=”false” requiresQuestionAndAnswer=”false” enableSearchMethods=”true” attributeMapUsername=”sAMAccountName” />
    </providers>
</membership>

This hooks up AD to the Membership API allowing us to query for basic AD information such as user name and email address.  The above configuration relies on AzMan which we’ll discuss next. 

Note: Ensure that the web site has Windows authorization enabled but not Anonymous otherwise IIS will not work properly.

Authorization Manager

Since we’re using application roles and AD understands Windows groups we need a translation layer.  Fortunately newer versions of Windows has us covered with Authorization Manager (AzMan).  AzMan allows us to define our application roles and store them in one of several different data stores.  IT administrators can then configure what AD users and groups are a member of each role.  For NuGet I’m going to use AzMan backed by an XML file but you could just as easily use a SQL database.  Getting the data store set up is pretty straightforward.

  1. Start Microsoft Management Console (mmc.exe).
  2. Add or browse to Authorization Manager.
  3. Create a new data store (i.e. AzManNuGetStore.xml)  Note: You must be in Developer Mode (menu Action\Option) to set up the store but you can switch back to Administrator Mode after the roles have been defined.
  4. Under the store add a new application called NuGet.  This value must match the role manager’s applicationName entry in the config file.
  5. Under Definitions\Role Definitions define the application roles – Authors, Admins
  6. Save the data store and optionally switch it back to Administrator Mode

At this point the application specific AzMan settings are completed but no AD users or groups are associated with the roles.  To assign an AD user or group to an application role open the store in MMC and go to the Role Assignments section.  In general dev leads and managers will likely be Admins while the developer groups will be Authors.

Place the store file in the root of the web site so it can be found.  The app pool identity will need read access to this file.  Now the membership provider needs to be pointed to the file.  To do that we’ll add two entries to the connectionStrings section.  The first entry (LocalPolicyStore) provides the path to the AzMan store.  This must match the role manager’s connectionStringName entry.  The second entry will be the AD connection information.  This must match the membership provider’s connectionStringName entry.

<add name=”LocalPolicyStore” connectionString=”msxml:{path}/AzManNuGetStore.xml” />
<add name=”ActiveDirectoryConnection” connectionString=”LDAP://{domain}/DC={subdomain},DC={subdomain}” />

Note that the path for the AzMan store uses URL slashes (/) between directory names.  That’s it.  Now ASP.NET is hooked up to AD for authentication and AzMan is providing the roles based upon the AD groups that are configured.

Additional Enhancements

As I mentioned in the earlier article there are some things about NuGet that don’t work well in a private environment, at least for me.  Therefore I went ahead and updated the code for them as well.  The next few paragraphs discuss some of these enhancements.  Refer to the original article for more information.

I wanted to make the option of using HTTPS configurable so the code didn’t have to keep changing.  Therefore I added a new gallery setting (Gallery.RequireHttps) to allow it to be toggled on and off.  It is set in the config file.  I updated the RequireRemoteHttpsAttribute to read the app setting and do nothing if it is disabled.  I didn’t bother caching the result but you could if you wanted to. 

public void OnAuthorization(AuthorizationContext filterContext)
{
    //MODIFIED: Don’t require HTTPS if it is disabled
    var requireHttps = Configuration.ReadAppSettings(“requireHttps”, x => (x != null) ? Boolean.Parse(x) : false);
    if (!requireHttps)
        return;

Email, by default, uses SSL.  I wanted to be able to turn this off as well.  Most of the email settings are stored in the database so that would be the logical place to change it but NuGet uses EF and updating the models and whatnot would be too much work so I added yet another gallery setting (Gallery.EnableSmtpSsl) to control whether or not to use SSL for email.  To use this setting I modified the ContainerBindings.Load (App_Start\ContainerBindings.cs) method to read the setting when it is creating the mailSenderThunk object.  Here’s the relevant code for reference.

var mailSenderThunk = new Lazy<IMailSender>(
    () =>
        {
            var settings = Kernel.Get<GallerySetting>();
            if (settings.UseSmtp)
            {
                var mailSenderConfiguration = new MailSenderConfiguration
                    {
                        DeliveryMethod = SmtpDeliveryMethod.Network,
                        Host = settings.SmtpHost,
                        Port = settings.SmtpPort,

                        //MODIFIED: Pull from the settings
                        EnableSsl = Configuration.ReadAppSettings(“enableSmtpSsl”, x => (x != null) ? Boolean.Parse(x) : false)
                    };

The newer versions of NuGet displays statistics on the home page.  The stats are handled via stats.js.  Unfortunately this file has a bug in the path that it uses to get the stats.  If the site is hosted as a full website then it will work but if it is an application under a website then the path is wrong.  Since the stats are handled in a Javascript file the change isn’t as simple as I’d like.  After talking with one of my Javascript experts we came up with this.

  1. Add a new Javascript variable to the shared layout page that stores the root path of the site.
  2. Modify stats.js to use the Javascript variable rather than using a rooted directory.
<script type=”text/javascript”>
var rootUrl = “@Url.Content(“~”)”;
</script>

//Inside stat.js:getStats
$.get(rootUrl + ‘stats/totals’, function(data) {

That should resolve any issues with the stats not displaying on the main page.  The site should now be configured to use AD authentication.  You should confirm the website is working properly as well as being able to view packages from Visual Studio and during builds.

What’s Not Covered

Some areas of NuGet have not been covered and may need to be changed if you use them.

  • Uploading packages from the web API might work since the web API doesn’t require any special privileges.  The user credentials that are passed should authenticate against the custom user service but some tweaks may be necessary.
  • Accessing any of the existing user APIs will probably not fail but won’t work as expected.  I originally started removing all the code but NuGet uses T4MVC which puts way too many hooks into the system for my taste.  In the end I left the existing API in place so I wouuldn’t have to touch every file.
  • Non-Active Directory networks won’t work directly since the user information is not available.  Specifically the email address would need to be obtained through some other means.

10 Responses to NuGet with Active Directory Support

  1. I don’t know if it’s just me or if everyone else experiencing issues with your site.
    It appears like some of the written text on your content are running off the screen.
    Can somebody else please comment and let me know if this is happening to them as well?

    This might be a problem with my browser because I’ve had this happen previously. Thanks

  2. This is very fascinating, You are an overly skilled blogger.
    I’ve joined your rss feed and look forward to looking for more of your wonderful post. Also, I’ve shared
    your web site in my social networks

  3. This website was… how do you say it? Relevant!
    ! Finally I’ve found something that helped me. Thanks!

  4. Good day! I simply would like to give a huge thumbs up
    for the great data you’ve gotten right here on this
    post. I shall be coming again to your blog for more soon.

  5. Remarkable! Its actually remarkable post, I have got much clear idea on the topic of from this piece
    of writing.

  6. I want to to thank you for this great read!! I
    definitely enjoyed every little bit of it.
    I have you bookmarked to check out new stuff you post…

  7. Howdy. Very nice site!! Guy .. Excellent .. Wonderful .. I will bookmark your web site and take the feeds also…I am glad to locate so much useful information here in the post. Thank you for sharing. [url=http://www.authenticretrojordanscheapsale.com]authentic retro jordans[/url] authentic retro jordans