Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 11

This all works so transparently because internally SqlMembershipProvider always calls the public ApplicationName getter whenever the provider needs this value. In the stored procedures for SqlMembershipProvider, almost every single stored procedure needs an application name. When the SqlMembershipProvider is building its SqlCommand objects, it fills in the application name stored procedure parameter with the value returned from the ApplicationName getter. Because the custom provider overrides this getter, the fact that the application name value is changing on each request is transparent to SqlMembershipProvider.

This approach is also safe from a concurrency perspective because the custom provider is depending on the HttpContext for the application name value. Because the context is local to each ASP.NET request, there is no chance that simultaneous page requests will tromp on each other’s application name. Even if two different page threads are simultaneously calling the ApplicationName getter, each thread will end up with a different value pulled from that thread’s associated HttpContext.

Although this sample demonstrates how to dynamically set the application name for a web application, the same technique is applicable to Web Service calls using .asmx files. The .asmx requests also have an HttpContext associated with the request — so the one difference is where you pull the application name from. Assuming that your web requests are submitted via HTTP, you could use the query-string, or you could use custom SOAP headers for storing the application name value. About the only tricky thing with overriding ApplicationName occurs if you want to use Membership from a “lights-out” application like an NT service. In this type of scenario, the same architectural approach applies, but instead of an HttpModule you will need to write code that determines the application name from some other data (for example, the request data that is queued to the service thread) and then initializes a shared memory location (for example, thread local storage being the most likely candidate) prior to calling into a custom provider.

If you are working with a portal application that can change its application context on each request, keep a few security points in mind. Even though it is trivial to make providers pick up a different application name on each request, remember that other features like forms authentication still work at the level of an ASP.NET application. If you validate credentials with a custom Membership provider, make sure that the forms authentication ticket you issue to one portal is not accidentally honored by another portal running in the same ASP.NET application. Similarly, if you write a custom Role Manager provider that overrides ApplicationName, make sure that your different portal applications don’t accidentally honor each other’s role information. In other words, customizing the ASP.NET providers is only one part of the broader architectural problem of making ASP.NET applications “act” like hundreds or thousands of virtual applications.

One other architectural solution has been proposed for dealing with dynamically setting the application name: why not just add applicationName as a parameter to every method on all of the ASP.NET provider and feature classes? Certainly, this is a technically viable option. There are problems with this approach though:

Developers would have to explicitly manage the application name throughout their code, whereas today the value gets set once and you can forget about it.

From a testing perspective, the test cost of having another parameter inside of every provider and feature method is rather expensive. Although for your own development it doesn’t seem like much overhead, for the ASP.NET team there is a nonzero cost each time a new method is added or a method signature widens.

462

SqlMembershipProvider

For both of these reasons, it is unlikely that future releases of ASP.NET will add an applicationName parameter back into the APIs. What is more likely is that the general approach outlined in this section will get baked into the provider APIs in some future release.

Summar y

The provider works in both ASP.NET and non-ASP.NET environments that are running at Low trust or higher. Remember, though, that the provider needs SqlClientPermission in partial trust environments and that this permission is not granted by default in Low trust. SqlMembershipProvider implements all of the security functionality available in the Membership feature. This includes advanced security features such as question-and-answer-based password resets as well as account lockouts when bad passwords or bad password answers are used. The provider stores user-related data in a combination of tables: some of which are common to all SQL-based providers, and some of which are specific to SqlMembershipProvider. Although there is nothing technically preventing you from using these tables directly, the expectation is that public APIs like the MembershipProvider class should be used for inserting and updating data. Only in the case where you need more extensive read-only access to Membership data should you query the database directly. ASP.NET ships with a number of SQL views that expose the data from the underlying tables for you to write SELECT queries against.

Although the default database engine used by SqlMembershipProvider is SQL Server 2005 Express, developers can easily change the LocalSqlServer connection string in machine.config to point the provider at any database server running SQL Server 7.0, 2000, or 2005. The only special logic that SqlMembershipProvider supports (and for that matter all of the ASP.NET SQL-based providers) for SSE is the automatic generation of a database containing the schema for all of the SQL-based features. Although this integration makes it very easy to develop using file-based webs in Visual Studio, you will probably be better off using the aspnet_regsql tool to manually install the schema when you develop against IIS6-based webs.

SqlMembershipProvider can also be extended by developers who want to integrate additional functionality. Because the provider is unsealed, most of the public properties and methods can be overridden by you. In this chapter, you saw how you could take advantage of this functionality to make simple changes in custom password generation and custom password encryption. More extensive changes allow you to extend SqlMembershipProvider with new features such as password history tracking and automatic unlocking of unlocked accounts. Last, with a just a few lines of code you saw how you can override the ApplicationName property to make SqlMembershipProvider work with multiple “applications” in portal environments.

463

ActiveDirector yMembership

Provider

The ActiveDirectoryMembershipProvider supports almost the entire set of functionality defined by the Membership API. You can create and manage users with either Active Directory (AD) or the standalone directory product Active Directory Application Mode (ADAM). Furthermore, you can use the provider in both ASP.NET and non-ASP.NET applications. Because the ActiveDirectoryMembershipProvider closely mirrors the SqlMembershipProvider in terms of functionality, the interesting parts of ActiveDirectoryMembershipProvider are how the provider works with the directory server and how certain Membership operations are mapped to AD and ADAM.

This chapter will cover the following aspects of ActiveDirectoryMembershipProvider in detail:

How the provider works with different directory structures

Provider configuration settings

Notes on various pieces of provider functionality

The ActiveDirectoryMembershipUser class

Working with Active Directory

Configuring ADAM to work with the provider

Using the provider in partial trust

Suppor ted Director y Architectures

Because the ActiveDirectoryMembershipProvider uses a directory store, you should understand the various domain architectures that it supports. The ActiveDirectoryMembershipProvider can

Chapter 12

work against either an Active Directory (AD) domain (both Windows 2000 and Windows Server 2003) or against what is called an application partition deployed in an Active Directory Application Mode (ADAM) server. Of the two directory server types, AD is the one with more varied options and, thus, requires a little more preplanning on your part.

The most important thing to keep in mind when using the AD/ADAM-based provider is that the provider treats AD and ADAM as Lightweight Directory Access Protocol (LDAP) servers. In essence, the provider is talking to these “databases” using LDAP commands. The provider does not interact with AD as an NT LAN Manager (NTLM) or Kerberos authentication service. This means that the provider does not return any kind of authenticated domain principal, and the provider cannot be used to generate a login token. It simply makes LDAP calls and LDAP binds to a directory server, and it returns the results of those calls. This behavior is sometimes a point of confusion for folks who think that ActiveDirectoryMembershipProvider generates security tokens and sets the security context on a thread. Because the provider is implementing the MembershipProvider base class, and the Membership API has no concept of returning security tokens or switching security contexts, the provider has no support for such operations.

The provider always works in the context of a directory container. This means that the provider is always pointed at the root of some container, and all provider operations occur within that single container, or in most cases through the hierarchy of nested child containers. For ADAM, this isn’t particularly surprising because ADAM servers are basically standalone LDAP directories. Even though a single ADAM server can host multiple application partitions (that is, these are sort of like mini-domains), the provider always needs to be pointed at a specific application partition when using ADAM. Typically, for developers working with ADAM, this is common practice — your application knows which application partition in ADAM it should be using.

However, for AD you can have a forest with multiple domains, and for many customers the forest infrastructure is very large and complex. If you use the provider in an AD environment, each configured provider can only be pointed at a single domain or at a specific container within a single domain. The provider does not support the concept of multidomain operations; realistically, the concept of seamless support for multiple domains is baked more into the authentication aspect of Active Directory as opposed to the LDAP aspect of AD.

Even though AD has a global catalog (GC) that can be used for LDAP queries that need to work with data from many domains, for the most part the ActiveDirectoryMembershipProvider does not make use of GC functionality. (There are a handful of verification checks where the provider will query the GC, but this functionality is all internal to the provider.) The provider also does not chase referrals, so you can’t set up user objects in one domain that are really referrals to objects in another domain and expect the provider to work. When using AD, you also cannot point the provider at a global catalog (that is, use GC:// in the connection string). If this were allowed the provider’s search and get methods would probably work, but all of the data modification methods would fail because GC replicas are read-only.

If you want to use the provider in a multidomain AD environment, you need to configure multiple provider instances — one for each domain or domain-container that you need to work with. In your application, you can implement logic that determines which domain it should work with, and your code can then select the appropriate ActiveDirectoryMembershipProvider instance from the Membership

.Providers collection. In this fashion, you can still effectively work in a multidomain environment with only a little extra code on your part.

466

ActiveDirectoryMembershipProvider

Note though that this means the machine on which the providers are running needs network connectivity to each of the different domains. For an extranet environment that has only a handful of domains, this probably isn’t an issue. However, if you have a more complex scenario where you need to access remote domains from an extranet environment, chances are that a web server in your DMZ is not going to have network connectivity to reach back into the internal corporate network and then communicate with some random directory controller. If you are architecting an application that needs to have multiple provider instances communicating with many different domains, make sure that your network topology will support this before you go too far down the coding path!

I have been making a number of references to containers for both AD and ADAM. The provider “knows” the context that it should be using based on the connection string configured for the provider. Just like the SQL providers, the ActiveDirectoryMembershipProvider uses a connection string, although in its case the connection string is an LDAP connection string. (You will see many examples of LDAP connection strings later in this chapter.) The connection string tells the provider which domain, directory server, or application partition it should work against, and the connection string also gives the provider enough information to know which container within the domain or directory server the provider should work with.

If you are working with ADAM, you always work explicitly with a container because you need to configure an application partition within which your user data is stored. As a result, the connection string you have in configuration when using ADAM always includes some container information in it. For AD this is not necessarily the case. In AD, you can point the provider at a domain, or a specific domain controller, without specifying a container. If you do this, the provider will default to using a combination of the default naming context for the domain and the “Users” container because this container is commonly available in AD domains. (User creation/deletion will occur in the Users container, whereas all other methods are rooted at the default naming context.) If you want to create your application’s users within only the Users container, then you can define your connection strings without an explicit container in the AD case. Of course, you also have the same ability in AD as you do in ADAM to create organizational units (OUs) and to specify these OUs as part of the connection string.

If your user data is spread across multiple containers, you have a few options for configuring the provider. If the user data exists in containers that are peers of one another, and all of the containers have a common parent, you can point a single provider instance at the parent container. Except for user creation and user deletion, the provider always performs subtree searches starting with the container determined from the connection string. For example, if you call GetUser on the provider and the provider is pointed at a parent container, then the provider will be able to find the user object if it is located in the parent container, or if it is located in any of the containers nested within the parent — regardless of how deep the nesting may occur.

If your application needs to create and delete users, then you will need to configure a separate provider instance for each separate container in which creation and deletion occurs. The reason for the different behavior is that for user creation and deletion there is no such thing as a subtree operation. When you create a user object it must be created in a specific location, and as a result the provider limits user creation and deletion to the container specified (or implicitly determined) on the connection string. For applications that have a number of OUs, though, it can be awkward to have to always manipulate different provider instances for each OU when calling common methods like GetUser or ValidateUser. Therefore, except for CreateUser and DeleteUser, all of the provider methods use subtree searches.

What happens if your application deals with multiple OUs sharing a common parent and you don’t want the provider to perform broad search operations across all of the OUs? If you intentionally want to limit

467

Chapter 12

all provider operations to a single OU, you can configure multiple provider instances and point each instance at a specific OU as opposed to a parent container. However, if you have a container structure that nests multiple OUs in a chain, and you want to limit the provider to only a single OU in the nesting chain, the reality is that any provider pointed at a nonleaf OU will still perform subtree searches down through all of the remaining OUs. About the only thing you can do for this scenario is to restrict access on a per-OU basis using different user accounts and then configure the different provider instances with different sets of credentials.

Provider Configuration

If you configure the provider with the minimum number of required configuration attributes, most of its functionality will work against existing AD installations. About all you need to get up and running is a provider definition and a valid connection string:

<connectionStrings>

<add name=”adconnection” connectionString=”LDAP://mydomain.dns.name”/> </connectionStrings>

<membership defaultProvider=” someprovider “> <providers>

<clear/>

<add name=”someprovider” type=”System.Web.Security.ActiveDirectoryMembershipProvider, ...” connectionStringName=”adconnection” />

</providers>

</membership>

It is pretty much guaranteed that for production applications, though, you will need to delve a little more deeply into the provider’s configuration. The section “Working with Active Directory” walks you through a number of the common configuration tasks for setting up the provider.

For now, take a look at the various configuration settings that are available in the <add /> element of the provider. The available settings fall into the following general groups:

Directory connection settings

Schema mappings

Search-specific settings

Membership provider settings

Directory Connection Settings

As with SQL provider, you need to at least supply a connection string so that the provider knows where it should read and write data. However, unlike SQL Server connection strings, there is no such thing as specifying explicit connection credentials inside of the connection string. Also connection security settings cannot be supplied inside of an LDAP connection string. As a result, the provider supports a number of additional configuration settings.

468

ActiveDirectoryMembershipProvider

The connection string that you use for the provider is placed in the <connectionStrings /> section. The provider references the connection string via the connectionStringName attribute. The connection string that you create supports a number of different formats, depending on whether you are connecting to AD or ADAM. For example, if you are running in a domain called foo.org and you have an AD domain controller called dcserver, the most prevalent forms of the connection string when connecting to AD look like:

LDAP://foo.org

LDAP://dcserver.foo.org

LDAP://foo.org/OU=SomeOU,DC=foo, DC=org

LDAP://dcserver.foo.org/OU=SomeOU,DC=foo,DC=org

However, if you are connecting to an ADAM server, you always need to have an application partition defined. Assuming that you have an ADAM server called adambox in the foo.org DNS namespace, you could use connection strings like:

LDAP://adambox.foo.org/O=myorg,DC=foo,DC=org

LDAP://adambox.foo.org/OU=SomeOU,O=myorg,DC=foo,DC=org

Unlike AD, ADAM servers can be listening on nondefault LDAP ports. If you install ADAM to listen on other ports, then the connection string can look like:

LDAP://adambox.foo.org:50001/O=myorg,DC=foo,DC=org

LDAP://adambox.foo.org:50001/OU=SomeOU,O=myorg,DC=foo,DC=org

If you do install ADAM on a nondefault port, and you plan on using secure connectivity to the ADAM server, you must make sure that SSL support has been configured properly on the ADAM server and on each of the machines that needs to connect to the ADAM server. If you don’t change the default port settings for ADAM, then SSL traffic by default occurs on port 636 (unsecured traffic occurs on port 389 by default). If your ADAM server uses these default ports, then you don’t need to specify a port number in the connection string.

Because both AD and ADAM can replicate changes across servers, the type of connection strings that you use will have an effect on when the provider sees changes made on other machines. For example, if you use an AD connection string that points only at a domain, it is possible that across a web farm different web servers will end up connecting to different domain controllers. This can lead to odd behavior where changes made to a MembershipUser on one server don’t show up immediately on other servers in your farm. Unfortunately, there isn’t anything the provider itself can do to mitigate the inherent latency of AD’s multimaster behavior. However, you can at least use connection strings that explicitly specify a server — in this case all provider instances that are pointed at the same server will see a consistent set of information.

One very important aspect of connecting to the directory server is connection security. From the sample connection strings, you saw that there is no indication of the secured state of the connection. You request security for the connection to the directory server via the connectionProtection provider configuration attribute. This attribute can be set to either None or Secure. By default, if you do not specify the attribute in your provider’s configuration, the provider will default to Secure.

469

Chapter 12

The reason that the attribute has only one of two settings is that attempting to expose the vagaries of negotiating secure connections with a directory server can quickly become very complicated. So rather than leaving it up to you to get things working, the provider simplifies the issue into a simple binary decision. Either you want connection security automatically established, or you don’t. Of course, there is a bit more complexity than that occurring underneath the hood. There are a number of mix-and-match combinations you can use with connectionProtection and the credentials used by the provider when connecting to the directory, though only a subset of settings really make sense.

connectionProtection=None for AD — This is not a combination you should ever use. In AD environments, any operations that set or change passwords must be done over secure connections, so with a setting of None, the provider will always fail when it attempts things like ChangePassword or ResetPassword. Also, you need to always use explicit connection credentials with this setting. Because AD has built-in support for automatically securing connections there isn’t much reason for ever using None in an AD environment.

connectionProtection=None for ADAM — You may find yourself using this combination in a development environment where you don’t have SSL certificates set up for your ADAM server and client machines. As with AD, you will need to configure the connection credentials explicitly to use the None setting. Note that for ADAM this means that you will be limited to using only ADAM user principals for the explicit credentials; domain credentials cannot be explicitly specified for ADAM when connectionProtection is set to None. Unlike AD, though, you can manually configure ADAM to allow password changes and resets to occur over unsecured connections. The section on “Using ADAM” later in the chapter shows you how to do this. Note though that I would not recommend using None in a production setting with ADAM; it only makes sense as a convenience early on during a development cycle. Even for development scenarios, at some point you should get SSL set up so that you are coding in an environment that more closely matches your deployment environment.

connectionProtection=Secure for AD — This is the default when connecting to an AD server, and it is the setting that you should use for most cases when working with AD. Internally, the provider will first make a check to see if SSL is supported on the directory server. If it is, all LDAP traffic will flow over Active Directory’s SSL port (that is, port 636). If SSL is not configured for AD, which is normally the case for at least intranet directory servers, then the provider will fall back and use signing and sealing for all LDAP traffic. If you have configured SSL in an extranet directory environment for example, then the provider will make use of SSL in preference to signing and sealing. Because the provider internally makes use of the Active Directory Services Interface (ADSI) API, it turns out that setting up SSL for AD environments gives the best performance when using the provider to connect securely to AD. Using SSL reduces the number of network connections that ADSI will open on behalf of the provider when making secure connections to AD.

connectionProtection=Secure for ADAM — This is the default when connecting to an ADAM server. As noted earlier, this setting will not work unless you have explicitly set up SSL on your ADAM server as well as on all machines that need to communicate with that server. The reason for this restriction is that unlike when connecting to AD, the provider only supports the use of SSL for securing network traffic with the ADAM server. Even if the ADAM instance is running on a server joined to a domain, the provider will not attempt to use signing and sealing.

When you set connectionProtection to Secure, you can find out the actual connection security that was chosen at runtime by querying the provider’s CurrentConnectionProtection property. This property returns a value from the System.Web.Security.ActiveDirectoryConnectionProtection enumeration that will tell you if SSL or signing and sealing were chosen.

470

ActiveDirectoryMembershipProvider

The last set of connection information that you can configure in the provider’s <add /> element is explicit connection credentials. The configuration attributes connectionUsername and connectionPassword can be used to explicitly specify the username and password to use when connecting to the directory server. If you don’t explicitly specify values for these settings the provider attempts to connect to the directory using either the process credentials from the IIS6 worker process, or the application impersonation credentials if application impersonation is in effect. If you explicitly specify the username and password, make sure to use protected configuration (discussed in Chapter 4) so that the credentials are not stored in cleartext on your production servers.

The format of the username differs, depending on whether you are connecting to AD or ADAM:

AD — You can specify the username in any format that is supported by Windows. The two most common username formats are the NT4-style format of DOMAIN\USERNAME and the user principal name format of username@domain.name.

ADAM — If you are connecting to an ADAM server with connectionProtection set to Secure, then you can explicitly specify either an ADAM user principal or a domain user account. For a protection setting of None though, only an ADAM user principal can be specified. An ADAM principal looks something like CN=Username,OU=AccountOU,O=MyOrganization,DC=corsair,DC=com. In the section on “Using ADAM,” there is a walkthrough of how to use an ADAM user principal when connecting to an ADAM server.

Directory Schema Mappings

By default the provider attempts to map the properties of the MembershipUser class to an appropriate set of default attributes on the user class in AD and ADAM. Some aspects of this mapping are configurable, whereas other aspects are not. The most important constraint is that ActiveDirectoryMembershipProvider always binds to objects of type user. Although in Windows Server 2003 and ADAM the ability to use inetOrgPerson was added, the provider currently only supports binding to objects of type user.

The following properties on MembershipUser have fixed schema mappings to attributes in the directory:

ProviderUserKey — This value maps to the objectSID attribute on the user object. As a result, you can get the user’s security identifier (SID) from the ProviderUserKey property and you can also retrieve MembershipUser instances using the SID as a key.

Comment — Maps to the comment attribute on the user class.

CreationDate — Maps to the whenCreated attribute on the user class.

LastPasswordChangedDate — Maps to the pwdLastSet attribute on the user class.

IsApproved — Maps to the userAccountControl attribute when using AD. Maps to the mDSUserAccountDisabled attribute when using ADAM.

IsLockedOut — Maps to msDS-User-Account-Control-Computed attribute when using AD on Windows Server 2003 or when using ADAM. This property is computed from the lockoutTime attribute and the directory’s account lockout duration setting when running against Windows 2000 AD (W2K’s schema did not include the msDS-User-Account-Control-Computed attribute). If you have configured the provider to support question-and-answer-based password reset, then the provider will also look at the custom tracking information for bad password answers when determining whether a user is considered locked out.

471