Adding An "All Authenticated Users" Feature to Forms-Based Authentication

Preamble...

One of the frequenly cited shortcomings of Forms-Based Authentication when compared to Windows Integrated Authentication is its lack of built-in support for an "All Authenticated Users" group similar to NT AUTHORITY\Authenticated Users.

This type of role comes in handy in a number of situations, most notably for provisioning public (but not anonymous) sites and public SSP permissions like "Create Personal Site". The most common workaround I've encountered has been a custom solution that ties in with user provisioning - create a role in the directory service, add all existing users to it, add any new users to it, and remove any users from it as they're deleted. For very large-scale directories, it's not practical to store many thousands of users in a single group, so the actual implementation might be one involving nested groups or a denormalized structure where group memberships are enumerated in the user object, but the same idea applies.

Problem...

My present customer's extranet MOSS implementation uses a very large LDAP directory with hundreds of thousands of potential users. To deal with their problems of scale, they long ago adopted the "list groups in the user object" approach. This made us unable to use the out-of-the-box MOSS Role Manager, which expects to find users enumerated in group objects, rather than groups enumerated in user objects. This led to a need for a custom LDAP Role Manager.

Like most MOSS implementations, they have a need for an "all authenticated users" role, but modifying user provisioning processes in their LDAP (of which there are many, but that's another story) wasn't a realistic option in terms of cost or schedule. Fortunately, the custom Role Manager opened the door to an elegant solution to the "All Authenticated Users" requirement.

Solution...

Once you've written your own role manager, it's pretty straightforward to embed your own "All Authenticated Users" feature into the component. I found it easiest to make this a configurable group name, but you could just as easily hard-code the value. Here's the walkthrough:

1. Create a private class variable (_LDAPAuthUsersGroup in the example below) for storing the group name.

2. Populate the group name variable in the Initialize method via a configuration setting.

3. In the GetRolesForUser method, append the "All Auth Users" group name to the roles list every time, thereby making every user a member of the group.

4. In the RoleExists method, always return true for the "All Auth Users" group name, thereby confirming its existence.

That's all there is to it. Snippets from relevant sections of the RoleProvider appear below.

namespace Company.MOSS.Auth
{
public class CustomRoleProvider : RoleProvider
{
/*
TO DO: put your other class variables here
*/

        private bool _LDAPEnableAuthUsersGroup;
private string _LDAPAuthUsersGroup;

        public CustomRoleProvider()
{
/*
TO DO: set other defaults here
*/

            this._LDAPEnableAuthUsersGroup = false;
this._LDAPAuthUsersGroup = string.Empty;
}

        public override void Initialize(string name, NameValueCollection config)
{
this._Name = name;
if (config != null)
{
try
{
/*
TO DO: initialize your other configuration variables here
*/

                    //check for the presence of the auth users group configuration setting
if (config["authUsersGroup"] != null)
{
//Extra validation to prevent blank auth users group
if (config["authUsersGroup"].Length > 0)
{
//making this case-insensitive - this might not apply to your directory service
this._LDAPAuthUsersGroup = config["authUsersGroup"].ToUpper();
//turning on a boolean flag indicating that we're using this feature
this._LDAPEnableAuthUsersGroup = true;
}
}

                    return;
}
catch (Exception ex)
{
throw new LdapProviderException(SPResource.GetString("LDAPProviderGeneralFailure",
new object[0]));
}
}
throw new ArgumentNullException("config");
}

        public override string[] GetRolesForUser(string username)
{
//this group list length works for me - might need to be different for you
List<string> rolesList = new List<string>(10);

            /*
TO DO: do whatever else you need to do to retrieve the "real" roles first
*/

            //if you're using the feature, then add the all auth users group to the roles list
if (_LDAPEnableAuthUsersGroup)
{
rolesList.Add(_LDAPAuthUsersGroup);
}

            return rolesList.ToArray();
}

        public override bool RoleExists(string roleName)
{
//if authenticated users functionality is enabled, check to see if this is that special group
if (_LDAPEnableAuthUsersGroup)
{
//making this case-insensitive - this might not apply to your directory service
string role = roleName.ToUpper();

                if (role.Equals(_LDAPAuthUsersGroup)) return true;
}

            /*
TO DO: do whatever else you need to do to check if "real" roles exist
*/
}

    }
}

Special Concerns:

REALLY, REALLY IMPORTANT: Make sure that the "All Auth Users" group name doesn't intersect with current or possible user/group names. That would be a horrible security hole - if someone created  a user or group with the same name as the All Auth Users group (whatever you choose to call it) and provisioned permissions to it on their site, all of a sudden EVERYONE would have that permission level. Very, very, very bad, potentially with legal/compliance consequences. There are three tactics I recommend for preventing this outcome:

  • Simple, fast solution #1: Create a placeholder group (and user) in the directory service with the same name as the All Auth Users group that YOU own. Never do anything with them other than ensuring that they don't get deleted. You're just putting them there to prevent anybody else from creating a group with the same name, assuming that the directory service enforces uniqueness. :)
  • Simple, fast solution #2: Give the All Auth Users group a name that falls outside the allowable domain of values for group names in your directory service - i.e. include special characters like # or \ if those are disallowed in group names; create an group name 100 characters long if group names are restricted to 50 characters in the directory service; etc.
  • Slow, fallback solution: This could also be used in combination with method #2, but adds a bit of overhead. Instead of the mindless algorithms for checking if the role exists and the list of roles for the user, add a check to see if that role name exists in the directory service. If so, go the safe route and use the "real" non-All Auth users group  instead AND throw a warning to the event log.

A secondary concern... what if you only want to make this feature available to selected users/sites? This is one of the reasons we went with a configurable setting for the group name - we didn't want your everyday user provisioning permissions to all authenticated users in their site. The design pattern for this is simple - go with an elaborate group name (like a GUID) and change it on a regular basis.