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.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.
{
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.
{
//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).
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.
<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 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.
@
{
ViewBag.Title = “Access Denied”;
}
<h1>Access Denied</h1>
<p> You have insufficient privileges to acces this page.</p>
{
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.
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!!
<allow users=”*” />
</authorization>
Add the necessary role and membership providers to use AD authentication.
<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.
- Start Microsoft Management Console (mmc.exe).
- Add or browse to Authorization Manager.
- 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.
- Under the store add a new application called NuGet. This value must match the role manager’s applicationName entry in the config file.
- Under Definitions\Role Definitions define the application roles – Authors, Admins
- 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=”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.
{
//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 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.
- Add a new Javascript variable to the shared layout page that stores the root path of the site.
- Modify stats.js to use the Javascript variable rather than using a rooted directory.
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.
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
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
This website was… how do you say it? Relevant!
! Finally I’ve found something that helped me. Thanks!
This post is invaluable. How can I find out more?
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.
Awesome! Its really amazing post, I have got much clear idea
about from this piece of writing.
Remarkable! Its actually remarkable post, I have got much clear idea on the topic of from this piece
of writing.
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…
Hi there, I read your blog like every week. Your writing style is awesome, keep up
the good work!
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