Tuesday, September 30, 2014

Implementing HTTPHandler and HTTPModule in ASP.NET

Introduction

This article aims at understanding the role of HTTPHandler and HTTPModule in ASP.NET applications. We will try to work on a basic example to see how these can be implemented.

Background

ASP.NET handles all the HTTP requests coming from the user and generates the appropriate response for it. ASP.NET framework knows how to process different kind of requests based on extension, for example, It can handle request for .aspx, .ascx and .txt files, etc. When it receives any request, it checks the extension to see if it can handle that request and performs some predefined steps to serve that request.

Now as a developer, we might want to have some of our own functionality plugged in. We might want to handle some new kind of requests or perhaps we want to handle an existing request ourselves to have more control on the generated response, for example, we may want to decide how the request for .jpg or .gif files will be handled. Here, we will need an HTTPHandler to have our functionality in place.

There are also some scenarios where we are ok with the way ASP.NET is handling the requests but we want to perform some additional tasks on each request, i.e., we want to have our tasks execute along with the predefined steps ASP.NET is taking on each request. If we want to do this, we can have HTTPModule in place to achieve that.

So from the above discussion, it is clear that HTTPHandlers are used by ASP.NET to handle the specific requests based on extensions. HTTPModule, on the other hand, is used if we want to have our own functionality working along with the default ASP.NET functionality. There is one Handler for a specific request but there could be N number of modules for that.

Using the Code

Let us try to understand these two concepts by writing a small application for each. What we will do is we will try to have a mechanism where we can process the web pages with extension like .bspx and .cspx. Although this is a very unrealistic scenario, a similar concept is used to have search engine friendly URLs so perhaps it's not that realistic either.

Note: The HTTPHandler example here is just for demonstration purpose, I am not recommending the use of HTTPHandlers for something that I am about to do now. HTTPHandlers should ideally be used to customize the handling of existing MIME types and not for serving search engine friendly URLs or non standard URLs.

Implementing the HTTPHandler

So with our problem definition, let us try to see how we can handle the request for .cspx pages using HTTPHandlers. First we need to have the handler class with us, so let us create the handler class.

Collapse | Copy Code

public class CspxHandler :IHttpHandler
{
    public bool IsReusable
    {
        get { return false; }
    }
 
    public void ProcessRequest(HttpContext context)
    {
 
    }
}

The class should have a method ProcessRequest and a property called IsReusable. The property tells whether this handler can be reused or not and the method will be called whenever a request for that type comes. But wait, Where have we defined the type of request where this handler should be invoked? This can be defined either in IIS, if we have a handler common to all the sites running on that server or we can configure it in web.config file, if the handler is specific for a website. Let's do that in web.config file for now.

Collapse | Copy Code

<httpHandlers>
    <add verb="*" path="*.cspx" type="CspxHandler"/>
</httpHandlers>

Here we registered our handler to specify that if any request for .cspx file comes, it should be forwarded to our handler.

Now, since we don't have any "real" files with .cspx extension, what we will do is we will handle the request for .cspx and in turn push the user to the corresponding .aspx file.

Collapse | Copy Code

public class CspxHandler :IHttpHandler
{
    public bool IsReusable
    {
        get { return false; }
    }
 
    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "text/plain";
 
        if (context.Request.RawUrl.Contains(".cspx"))
        {
            string newUrl = context.Request.RawUrl.Replace(".cspx", ".aspx");
            context.Server.Transfer(newUrl);
        }
    }
}

Whenever a request for .cspx file comes, we will handle it in our handler and show the corresponding .aspx file instead. Let's see how it works.

Note: I have also changed the startup page name to Default.cspx but there is no page like that. I want my handler to handle that and show me the actual default page.

Important: I reiterate, This example is just for illustration. This is not how HTTPHandlers should be used at all. HTTPHandlers should ideally be used to customize the handling of existing MIME types.

Well the pages seems to be working fine and the user will see .cspx URL for his request. But there is one problem. The way we wrote our handler is not good to handle the postback. If I add a button on any of these pages and do a postback, the original URLs will be visible. So it is not a good solution to the problem but it sure demonstrated the way Handlers can be used.

Implementing the HTTPModule

How do we solve the problem we just saw. Well, our application needed URL rewriting and HTTPHandlers are a bad solution for that and should never be used for that. So perhaps the guys using this technique to have search friendly URLs should rethink their strategy. SO how can we solve this problem really.

Let us look at the requirement again, All we needed was to show the user URLs which are different than the real URLs and process the real URLs internally. So we don't need custom handlers, we are ok with the way ASP.NET engine is handling these requests but we need custom activities to be done during the processing phase. So it looks like we can solve it using HTTPModule.

So let's go ahead and write an HttpModule that will:

1.      Check for file extension on request.

2.      If it finds a .bspx extension it changes it to .aspx (or find real URLS if we are implementing search friendly URLs)

3.      It will pass the request to the default handler, since the page is still aspx.

4.      Once the response is generated, it will write back the original .bspx URL to users browser.

Collapse | Copy Code

public class MyBModule : IHttpModule
{
    public void Dispose()
    {
 
    }
 
    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
        context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
        context.EndRequest += new EventHandler(context_EndRequest);
        context.AuthorizeRequest += new EventHandler(context_AuthorizeRequest);
    }
 
    void context_AuthorizeRequest(object sender, EventArgs e)
    {
        //We change uri for invoking correct handler
        HttpContext context = ((HttpApplication)sender).Context;
 
        if (context.Request.RawUrl.Contains(".bspx"))
        {
            string url = context.Request.RawUrl.Replace(".bspx", ".aspx");
            context.RewritePath(url);
        }
    }
 
    void context_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        //We set back the original url on browser
        HttpContext context = ((HttpApplication)sender).Context;
 
        if (context.Items["originalUrl"] != null)
        {
            context.RewritePath((string)context.Items["originalUrl"]);
        }
    }
 
    void context_EndRequest(object sender, EventArgs e)
    {
        //We processed the request
    }
 
    void context_BeginRequest(object sender, EventArgs e)
    {
        //We received a request, so we save the original URL here
        HttpContext context = ((HttpApplication)sender).Context;
 
        if (context.Request.RawUrl.Contains(".bspx"))
        {
            context.Items["originalUrl"] = context.Request.RawUrl;
        }
    }
}

Also we need to register our module so that it can be invoked, we will do that in our web.config file.

Collapse | Copy Code

<httpModules>
    <add name="MyBModule" type="MyBModule" />
</httpModules>

And now let's run the application:

So here we solved the problem of URL reverting back to the original on postback. This is also the ideal way of doing that.

Points of Interest

In this article, we saw how we can implement a basic HTTPHandler and HTTPModule. We saw each of their roles in page processing frameworks. We worked on an example that tried to solve the URL rewriting first the wrong way by using HTTPHandler (but we understood how to write HTTPhandler) and then the right way of doing URL rewriting using HTTPModule (we got to understand that too.

The emphasis of this article was solely on understanding how we can have HTTPHandlers and HTTPModules working. The example is a little unrealistic and perhaps a little misleading too but since I made that point really clear, it shouldn't be a problem.

Before wrapping up, there is one last thing that we should know about handlers. It is also possible to handle the request asynchronously. ASP.NET provides a mechanism for creating asynchronous handler and then increases the performance of a web page (implements the IHttpAsyncHandler do that).

Reference : http://www.codeproject.com/Articles/335968/Implementing-HTTPHandler-and-HTTPModule-in-ASP-NET

Friday, September 26, 2014

Understanding SharePoint 2010 Claims Authentication

This post is intended to fill some gaps and provide a foundation to understand components in the claims model and how these components work together. Claims will provide a huge benefit which I will outline some of those benefits below. SharePoint 2010 has a new approach to authentication\authorization. Instead of using the classic (Integrated) authentication method, it's possible to authenticate and authorize users against external Identity Providers. No longer, are we limited to directory repository's like Active Directory. In fact, it's possible to create custom identity providers and SharePoint will trust and leverage that Identity Provider thus granting external user access to a SharePoint site/document etc...

Claims

An identity provider makes claims about a user. A good example of an identity provider is Live ID. So Live ID will claim to have attributes and their values. For Example:

Identity Provider "provider of the attributes" contains username attribute containing DanCan. A custom identity provider created by a hacker also contains an account with username attribute named DanCan. Both identity providers are making claims about a user. The consumer "SharePoint 2010" must choose which claim it's going to trust. SharePoint 2010 by itself will never trust either claim without being told to do so. In order for SharePoint to use a claim, it must first trust that claim which is setup by you the SharePoint administrator. If claims are trusted, then SharePoint can authenticate and authorize over that claim.

 

STS

STS is built on Geneva framework which is now called Windows Identity Foundation. The STS (Security Token Service) core responsibility is issuing, managing, and validating security tokens. An STS resides on both an identity provider and SharePoint. STS is built on top of the shared services framework which is why it's listed as a service application within Central Administrator\Manage Service Applications page:

 

Above, STS is composed of a web service and runs on every SharePoint server.

 

 

Authentication

The authentication type is setup at the Web Application level when creating a new SharePoint web application. It's possible to choose either classic authentication or Claims authentication. Each one is discussed below:

Classic

Active Directory authenticates a user, provides an NT Token. The token is used to authenticate to SharePoint. SharePoint consumes that token and it's converted into an SPUser object for authorization.

Note: Authorization is the process of determining what level of access an authenticated user has to a secured resource such as a Site, Document library etc.. The authorization mechanism hasn't changed in SharePoint 2010 and we ultimately still use an SPUser object to authorize.

Claims

After a trust is established between SharePoint and an Identity provider, web applications can be set with Claims authentication type instead of classic. If a client attempts to authenticate to a claims aware web application, SharePoint redirects a client to the associated trusted identity provider. The identity provider authenticates clients and provides a security token. That token could be either of the following:

· NT Token

· SAML Token

This security token is this passed to SharePoint STS. In short, the STS will validate the token "Claims Based Identity" and generate a new security "SAML" token back to the client. This token is generated by SharePoint and for SharePoint. The client sends this SAML token to SharePoint to prove that he/she is officially authenticated. SharePoint validates and authenticates user and an SPUser object is created and is used for authorization.

Steps for Claims Sign-In:

1. Client hit SharePoint site via HTTP (Get)
2. SharePoint redirects client to Identity Provider in order to get a security token
3. Client attempts to authenticate to trusted Identity Provider
4. The identity provider's (Security Token Service) will validate the username and password and provide a security token to a client.

Note: A security token could be a Windows NT Token, SAML token, or FBA token

5. The client has a security token (authenticated) and submits it to SharePoint STS "Security Token Service"
6. SharePoint STS receives security token from client and determines if we trust the issuer of that token "Identity Provider"
7. STS then performs claims augmentation
8. STS issues client new SAML token 
9. Client request resource "site" with new SAML token 
10. SharePoint consumes SAML token, "validates authentication successful", and builds an SPUser object in order to authorize to the secured resource

Mixed Authentication

In SharePoint 2007, to use additional authentication provider, you had to extend the web application and drop it in a different zone so it would contain a different URL. SharePoint 2007 wasn't flexible in terms of specifying multiple authentication types in a single un-extended web application.

Multi Authentication

In SharePoint 2010, it's possible to configure multiple authentication types for a single web application. This provides 2 benefits:

1. No longer required to extend web-application for the purpose of adding additional authentication types

2. Can have a single web application use multiple authentication types which provides the ability to serve a single URL!

Note: You can still extend web-applications and assign one or more authentication types to it if a business justification calls for that.

 

 

FBA

FBA users no longer uses an ASP.Net identity. FBA is now claims aware and the SharePoint STS facilitates the authentication process. Once user is authenticated, the SharePoint STS provides a SAML token to the client.

Note: When creating a web application designated for FBA, you must specify claims authentication type.

STS (federated equivalent of a domain controller) "issues tokens"

Basic FBA Sign-in process:

1. User signs in via FBA with credentials
2. SharePoint STS calls membership provider to authenticate
3. SharePoint STS calls role provider to get all the roles for the user
4. Post successful authentication, a SAML token is generated by the SharePoint STS and passed back to the user
5. The user then authenticates to SharePoint with SAML token and authentication is officially completed

For setup steps, please see Post for more details.

 

How Claims works with Services

Accessing Internal Services

Within a Single Farm:

The classic example is a user performing a search. The WFE's (Server1) search web part talks to service application proxy. The associated search service application proxy calls the local STS to get a SAML token for the user. Once SAML token is collected, the search service application proxy then calls a server running the Query Processor via WCF call. I'll call this server, "Server 2". Server 2 receives the incoming request and validates the SAML token against its local STS. Once validated, Server 2 connects to various components to gather, merge, and security trims search results. Server 2 sends the trimmed search results back to Server 1 which are then presented to the user.

Accessing External Services

SharePoint 2010 STS can manipulate a SAML token in order to present it to an external web service. The way it presents the identity depends on the type of external web service. The goal is preventing the additional prompt for credentials so that a full Single Sign-On (SSO) experience is possible. The STS is comprised of the WIF "Windows Identity Framework" and also the C2WTS. Each component is used dependent upon the type of external service accessed.

C2WTS = Claims to Windows Token Service

If accessing a native windows application that expects a Kerberos ticket. Within SharePoint STS, we use C2WTS to use existing SAML token in order to create a windows token (Kerberos ticket) to authenticate.

http://msdn.microsoft.com/en-us/library/ee517278.aspx

SharePoint STS

Can be used to just issue SAML token to pass to external systems that support SAML tokens

Secure Store Service

SharePoint can be used to connect to a legacy LOB systems which requires credentials. (SSS) Captures credentials and uses them on web service call to login and go inside.

http://msdn.microsoft.com/en-us/library/ee557754.aspx

 

Setting Up SharePoint 2010 forms-based authentication for claims based web applications

 

The steps in the most simplistic form are the following:

1. Create a forms-based\claims Web application to use an LDAP Provider using Central Admin

2. Configure the LDAP Web.Config files for the Central Administrator (web application), Security Token Service (web service), and FBA claims-based (web application).

3. Within User Policy for the newly created FBA\Claims Web Application, Add site collection owner and grant full control.

4. Finally, login to FBA site as site collection owner and grant user permissions to access site

We released a technet article “beta 2” version of how to accomplish this setup using the OfficeServer Ldap Provider. I created this blog in order to fill the gaps and provide some further insight on how to set this up properly. The technet article which covers the first two steps above is located here:

http://technet.microsoft.com/en-us/library/ee806890(office.14).aspx#section2

Note: This has been tested on Beta 2 version. I’ll update the blog when later builds are released to general public if changes are required.

 

FBA Setup Gotcha’s (three of them)

Gotcha # 1: Steps 3 and 4 are required

I will discuss steps 3 and 4 above in more detail now since they are missing from the article. Once you finish step 1 and 2 from the article, follow step 3 and 4 here:

 

Step 3 - Within User Policy for the newly created FBA\Claims Web Application, Add site collection owner and grant full control. Steps for this are the following:

1. Launch Central Administrator and select “Manage web applications” under Application Management

2. Select the FBA-Claims based web application and select User Policy from the ribbon

3. Select Add Users, select default zone and hit Next

4. Select the Address book button and add the site owner “Add the account under “User: ”

Note: These are both the same account. You are only required to add the account under “User:” since it’s the one enumerating via the LDAP provider.

5. Grant “Full Control under Permissions and hit Finish button.

 

Step 4: Login to FBA site as site owner and grant users access to the site.

1. Login to FBA site as site owner

2. Select Site Actions\Site Permissions

3. Select the Group you want

4. Select New, Add users to group and hit the address book

5. Select the ldap account, Add, and hit OK

 

 

Gotcha # 2: Web.Config setup

The Technet article walks you through the setup nicely but a couple of things I want to point out. Mainly, misconfigured web.config files. First, treat each web.config file your configuring unique. Not all web.config files are the same. Don't copy output of one and paste it into another one and expect it to work.

Attributes to be aware of when configuring ldap provider in each web.config:

UserContainer - This attribute should look like: userContainer="CN=Users,DC=domain,DC=com"

If UserContainer attribute doesn’t contain a valid DN, you might see the following in the ULS logs during failed logon attempt:

12/29/2009 14:04:54.15  w3wp.exe (0x1118)        0x1374  Office Server     Shared Services                olgq       Exception                System.DirectoryServices.DirectoryServicesCOMException (0x80072030): There is no such object on the server.       at System.DirectoryServices.SearchResultCollection.ResultsEnumerator.MoveNext()     at System.DirectoryServices.DirectorySearcher.FindOne()     at Microsoft.Office.Server.Security.LDAP.FindOneObject(DirectoryEntry searchRoot, String filter, SearchScope scope, String[] propertiesToLoad, ResultPropertyCollection& entryProperties)     at Microsoft.Office.Server.Security.LdapMembershipProvider.GetUserAttributeBySearchProperty(String searchValue, String searchProperty, String returnAttribute)

 

groupFilter and userFilter - These two attributes sit under the LdapRoleProvider.

In the technet article, these use different filters within different web.config files.

For example:

Setting these two attributes in the Central Admin web.config looks like:

groupFilter="((ObjectClass=group)"

userFilter="((ObjectClass=person)"

Setting these two attributes within the Claims based FBA Web Application web.config looks like:

groupFilter="(&amp;(ObjectClass=group))"

userFilter="(&amp;(ObjectClass=person))"

It's easy to see the difference in these filters. If your ldap filters are invalid, you will typically get the following exception within the corresponding ULS log during a failed attempt to login via FBA:

12/29/2009 11:52:19.43  w3wp.exe (0x0B04)        0x0F28  Office Server     Shared Services                olgz        High                LdapRoleProvider.GetRolesFor() exception: {0}.System.ArgumentException: The (&(((ObjectClass=group))(member=CN=userx,CN=Users,DC=Domain,DC=com)) search filter is invalid.     at System.DirectoryServices.SearchResultCollection.ResultsEnumerator.MoveNext()     at Microsoft.Office.Server.Security.LdapRoleProvider.GetRolesFor(String userOrGroupDN, DirectoryEntry groupContainer, LdapDistinguishedNameManager ldapDnManager, List`1& userRoles)   

 

 

Gotcha # 3: Additional step required for Standalone installs

If you run through the above steps with Standalone installs, you need to add one additional step or you will see this in the corresponding ULS log during a failed attempt to login via FBA:

12/29/2009 11:50:02.81 w3wp.exe (0x1868) 0x1AB4 Office Server Shared Services olgq Exception System.DirectoryServices.DirectoryServicesCOMException (0x80072020): An operations error occurred. at System.DirectoryServices.SearchResultCollection.ResultsEnumerator.MoveNext() at System.DirectoryServices.DirectorySearcher.FindOne() at Microsoft.Office.Server.Security.LDAP.FindOneObject(DirectoryEntry searchRoot, String filter, SearchScope scope, String[] propertiesToLoad, ResultPropertyCollection& entryProperties) at Microsoft.Office.Server.Security.LdapMembershipProvider.GetUserAttributeBySearchProperty(String searchValue, String searchProperty, String returnAttribute)

One additional step is required and that is adding a couple of entries to the STS (Security Token Service) web.config file. You will need to add both connectionUserName and connectionPassword.

For example (see Red bold entries below)

  <system.web> 
    <membership> 
      <providers> 
        <add name="membership" 
             type="Microsoft.Office.Server.Security.LdapMembershipProvider, Microsoft.Office.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=94de0004b6e3fcc5" 
             server="newyearDC.contoso.com" 
             port="389" 
             useSSL="false" 
             userDNAttribute="distinguishedName" 
             userNameAttribute="sAMAccountName" 
             userContainer="CN=Users,DC=Contoso,DC=com" 
             userObjectClass="person" 
             userFilter="(&amp;(ObjectClass=person))" 
             scope="Subtree" 
             otherRequiredUserAttributes="sn,givenname,cn" 
             connectionUsername="contoso\administrator" 
             connectionPassword="password" /> 
      </providers> 
    </membership> 
    <roleManager enabled="true" > 
      <providers> 
        <add name="rolemanager" 
             type="Microsoft.Office.Server.Security.LdapRoleProvider, Microsoft.Office.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=94de0004b6e3fcc5" 
             server="newyearDC.contoso.com" 
             port="389" 
             useSSL="false" 
             groupContainer="DC=Contoso,DC=com" 
             groupNameAttribute="cn" 
             groupNameAlternateSearchAttribute="samAccountName" 
             groupMemberAttribute="member" 
             userNameAttribute="sAMAccountName" 
             dnAttribute="distinguishedName" 
             groupFilter="(&amp;(ObjectClass=group))" 
             userFilter="(&amp;(ObjectClass=person))" 
             scope="Subtree" 
             connectionUsername="Contoso\Administrator" 
             connectionPassword="password" />
 
      </providers> 
    </roleManager> 
  </system.web>

Try it out !!