9 Steps to Secure Forms Authentication

The days of Web coders being able to ignore security are gone. Here’s what you need to know — and do — to authenticate users securely.

asp:feature

Languages: VB

Technologies: Security | Forms Authentication

 

9 Steps to Secure Forms Authentication

The days of Web coders being able to ignore security are gone. Here's what you need to know - and do - to authenticate users securely.

 

By Beth Breidenbach

 

"Secure coding for Web apps? Boring!"

 

"Don't the network guys take care of this stuff with their firewalls and Intrusion Detection Systems?"

 

"As long as I can code a login screen correctly, haven't I met my secure coding requirements?"

 

Does any of this sound familiar? It reflects a common mind-set. Security tends to evoke images of network perimeters and file access rules. And let's face it, how many developers really want to know the ins and outs of Access Control Lists (ACLs)?

The truth is, though, the days of Web coders being able to ignore security are gone. Modern Web sites expose enterprise-class databases, allow execution of client-side script, and accept input from anonymous users - some of whom are not as friendly as we'd like.

 

This article is not your typical "how to code simple forms authentication" article. Instead, we'll take a close look at the choices you face at each step in the authentication process, the implications of those choices, and how to code them safely.

 

Step 1. Determine Whether to Use Forms Authentication

Granted, this is a strange point to make in an article about forms authentication. Still, you often code applications the same way out of sheer habit. It's important to remember that the .NET authentication framework also provides excellent support for Passport and Windows authentication.

 

In particular, consider using Windows authentication for intranet and extranet applications. Windows authentication leverages the security of the underlying operating system and scales well, although it requires you to establish a Windows NT account for each user. It allows the application to impersonate the users' Windows NT accounts, operating under the same access control restrictions as the users themselves. By using Windows authentication in these scenarios, you avoid recoding authentication and authorization controls that exist already.

 

Step 2. Assess the Level of Security Required

Just as the most appropriate authentication mechanism varies between applications, the level of security required varies as well. Security has a cost attached to it - programming time, run time overhead, and so on. Take a moment to assess what secrets your Web application needs to guard and in which portions of the Web site those secrets reside.

 

At a high level, you can categorize secrets into two major categories: customer and business. Customer secrets are the information entrusted to you by your users. These secrets might be as simple as a username and password, or your users might include identifying data such as credit cards and e-mail addresses. You have an obligation to ensure your applications don't expose customers' data to the rest of the world.

 

Consider the simple case of your customer's username and password. What is the likelihood that a customer uses the same password for your site as for his or her e-mail address? Pretty likely. On the other hand, "personalization" data about how the customer would like the Web page displayed is often more innocuous and might not require as much security. Identify the various types of information your customer has entrusted to you and determine the risk level associated with disclosing that data.

 

Business secrets include the line-of-business systems your organizations use, the databases that support them, your company's network and database configurations, and more. As with customer information, you need to assess what types of information are accessible and what level of risk is associated with disclosing that information, then code an appropriate level of security into your site.

 

The end result of this review should be a brief document identifying the types of information stored and the security choices made. This document can be quite useful six months later when someone asks, "why are we accepting the performance overhead of encrypting user passwords?" We all know how challenging it can be to revisit old decisions and remember the details. Document these decisions.

 

Step 3. Pick a Coding Environment

This exercise has a strong payback because it provides the information to code correctly for the environment that you will actually install - which often is quite different from the development environment.

 

You have many options available within ASP.NET forms authentication. For example, you can choose to let ASP.NET store the users' credentials for you in the web.config file, or you can place the credentials elsewhere. You can insist on cookies, or you can allow authentication without them. The correct development choices depend on the installed application environment. Few developers enjoy re-coding portions of an application at the last minute - it's best to identify the choices at the beginning. The table in FIGURE 1 runs through the choices to consider.

 

Category

Environmental Question

Implications

Scalability

Will you use multiple IIS servers for this application?

All servers need access to the same credential data. Store in database or synchronized config files.

Scalability

Must new user information be available to all servers immediately?

Time delay in synchronization will not be acceptable. Store in database unless few updates occur.

Scalability Performance

Will the registration information change frequently?

File synchronization becomes more difficult. Usually stored in database.

Security Level

Is the user information so innocuous that it requires no security, and are passwords not user-defined?

Encryption overhead might not be required. Clear text data in web.config file might be acceptable. (This is rare.)

Security Level

Do portions of the application allow differing views and functionality depending on the role of the authenticated user?

Implement role-based authorization as second layer of security.

Security Level

Is it critical that all client-server communication be encrypted?

Consider SSL for the entire site.

Performance

Do performance considerations override the need to encrypt all traffic?

Enable SSL for only the login page's folder.

Browser Clients

Are you required to accommodate users who haven't enabled cookies?

Implement cookieless authentication.

User Management

Should the cookies expire?

Configure expiration parameters to correct number of minutes.

User Management

Should the credential information persist beyond browser closure?

Configure persistence. If also requiring expiration, additional coding is required.

User Management

How much user data do you wish to track between pages?

Entire authentication cookie should be less than 4K. If you need more than 4K total, user data must be stored elsewhere.

Content

Does the entire application need to be authenticated?

Portions might be allowed to the anonymous public. Configure root and subfolder web.config files accordingly.

Content

Do all pages need to offer the ability to log in?

Add login link to anonymous pages.

Content

Do all pages need to offer the ability to log out?

Add logout link to anonymous pages, display when user is logged in.

Content

Are there non-.aspx files we need to secure within the target directories?

Optionally map file extensions to ASP.NET ISAPI filer.

FIGURE 1: Here is a laundry list of questions to ask yourself when implementing your authentication environment. Making these decisions from the beginning will help direct your coding efforts.

 

What should be the end result of this exercise? I recommend updating the document you created in Step 2 with the information you've gathered here. You will have one single document describing the authentication environment that must be coded, as well as the rationale behind the choices you made. Now you are ready to code.

 

Step 4. Set Up the web.config Files

Your ASP.NET Web application's authentication begins with correct setup of the web.config files. Each application has at least one web.config file, placed at the application's root. You should place the application's overall security configurations in this root-level file.

 

If you have determined only a few subdirectories require authentication, you still place all security configurations at the root level and use the appropriate XML element to indicate that most directories will allow anonymous access. A separate web.config file, containing the appropriate element, is placed in each directory requiring authentication. FIGURE 2 shows the Solution Explorer view of this article's downloadable example application. As you can see, the "Secure" directory has its own web.config file.

 


FIGURE 2: This ASP.NET application has a Web.config file at its root that allows anonymous access to most of the application. The directory named "Secure" has its own web.config file configured to deny anonymous access.

 

The listing in FIGURE 3 shows typical root web.config security elements for an application that allows anonymous access to most of the site, requiring authentication only to a subdirectory. You need be concerned only with the and elements in the file (the full web.config file is available for download at the asp.netPRO Web site; see the end of this article for details).

 

The web.config file in FIGURE 3 shows a configuration appropriate for a single-server Web site with few changes to user credentials. This allows anonymous access to the root Web site, securing only certain subdirectories.

 

    

  loginUrl="Login.aspx"

  timeout="15"

  path="/ "

  protection="All">

  

          

           password="26E13F4ECF73413D5DC9A99FDDD34C84" />

  

   

 

    

FIGURE 3: Here is a configuration for a single-server Web site with few changes to user credentials. It displays typical web.config security elements.

 

The mode attribute indicates you are using forms authentication. The details of how forms authentication should be implemented are determined by the attributes found within the element:

 

  • name: The name used for the authentication ticket, which is the cookie storing the user credentials. If you don't specify a name it will default to .ASPXAUTH. This can cause conflicts if more than one ASP.NET application is running on the same server. It's a good practice to always provide a name for the authenticate ticket.
  • loginUrl: the location of the login form. ASP.NET can automatically redirect requests to this page whenever authentication is required.
  • timeout: Specifies the number of minutes after the last request is received before the authentication ticket expires. The default value is 30 minutes. Note that if you implement persistent authentication tickets this timeout value will be ignored. Additional coding is required when combining persistence with the ability to time out cookies. You will see an example of this code later.
  • path: the path for the authentication ticket cookie. Using this directive can sometimes be problematic as some browsers are case-sensitive. Most applications use the "/" path to simplify matters.
  • protection: The level of protection to apply to the authentication ticket. Values may be Encryption, Validation, None, or All. Details of these parameters can be found in the .NET documentation. Essentially, Encryption prevents viewing a ticket's content, while Validation allows ASP.NET to verify that the authentication has not been tampered with. Why would you choose a protection value other than "All"? Well, for example your application may already be encrypting all traffic via HTTPS. You wouldn't need the additional overhead of encrypting the ticket at the application level.

 

You can use the element to store user credentials directly within the web.config file. This is not recommended for high-update or multiserver applications.

 

If you're wondering why the directive for a secure site is allowing anonymous access to the site by specifying , it's because this Web site is configured to require authentication for only one subdirectory. That subdirectory has its own web.config file with this entry to deny all unauthenticated access:

 

  

 

Step 5. Establish Encryption

(Note: For a better understanding of encryption, download the resource list from the asp.netPRO Web site; see the end of this article for details.)

 

Encryption keys are used for both the decryption and validation of the authentication ticket. If the authentication ticket is encrypted (through the protection element setting in web.config's forms element) the correct key must be available to decrypt the data prior to attempting to validate it. If the authentication ticket is to be validated, the server concatenates a validation key to the ticket, computes a Message Authentication Code (MAC), and attaches the MAC to the ticket cookie. When the Web server re-receives the ticket, it re-calculates the cookie's MAC based on its current contents and compares it to the MAC that was sent with the cookie.

 

If the contents have been tampered with, the two MACs will not match up. Unfortunately, if the two IIS servers in a Web farm use different decryption or validation keys, the authentication process will fail. Because ASP.NET's default behavior is to generate unique encryption keys for each Web server, you need to use the directive to specify a consistent keyset across the entire Web farm.

 

You can set the element at the server level (in machine.config) or at the Web application level (in web.config). This code is taken from a web.config file:

 

validationKey="980D2378779BF031E3F575AF3B4E8CF7BA60AF5EC1421

71BF77FC82FA87D7C5C22F96112A8E19A32C7BAFEED049B1D8ABA32F9B79

0AEDD3473C4F7B195FFAD31"

decryptionKey="0EB739C6431A601B9ED74D724645F5068B81E54F86C71

814"

validation="MD5"

/>

 

As you can see in the previous file, I specified encryption keys for both validation and decryption. Let's look at the details of each.

 

You may assign a validationKey attribute either a key value or the term "autogenerate," which is the default value. A validation key may be between 40 and 128 characters long. The range available for each character is the normal hex range of 0 through F. For obvious reasons, the documentation recommends using the maximum length possible for this key. Note that this key is also used for validating your application's view state values.

 

When defining a validation key, you must also define which encryption algorithm to use when validating the authentication ticket. You do this by setting the validation attribute to either SHA1, MD5, or 3DES. This same setting is used for validating view state values, with one exception: If you specify 3DES (a.k.a. triple DES encryption), it will be used only for view state; SHA1 will be substituted for 3DES for the validation process.

 

A decryptionKey attribute is also assigned either autogenerate or an encryption key value. The defined value may be either 16 or 48 characters long. (Note that this contradicts some errant documentation stating the key may be 40 to 128 characters long.) If the key is 16 characters long, ASP.NET will use DES encryption. If the key contains 48 characters, triple DES (3DES) encryption will be used.

 

How do you generate adequately random keys for this config file? Microsoft Support Services article # Q313091 details how to use the RNGCryptoServiceProvider class to generate keys of the appropriate length and randomness. This article's code download provides a working example of this code. FIGURE 4 shows an encryption and validation key generated by this application.

 


FIGURE 4: Use the RNGCryptoServiceProvider class to generate sufficiently random keys for your encryption algorithms. The code for this page is available in the article's download.

 

Step 6. Collect User Credentials Safely

You doubtless have seen numerous examples of login forms before. Unfortunately, many of the older examples were concerned only with explaining how to get login information into a database query. What was often neglected is the importance of validating the user inputs before putting them to use.

 

Inadequate input validation, combined with the use of dynamically built SQL queries, have opened numerous Web sites to the risk of SQL Injection, Cross-Site Scripting, and other attacks. A whole class of attack vectors depend upon poor input validation for their success - your application should be defended at every point of user input. Risks from such attacks range from simple user data theft to hackers gaining complete control of the database server. For information about attack vector classes for Web applications, see the Open Web Application Security Project's Attack Component list at http://www.owasp.org/asac.

 

As Michael Howard explains in his book Writing Secure Code (Microsoft Press), we must assume user input is malicious unless it can be proven otherwise. For example, your policy might be that passwords are between five and 15 characters long and must have at least one number. If so, your login page should validate that the user's input meets the criteria before it ever attempts to use it.

 

The code in FIGURE 5 uses the Regex object to validate user input. The two functions, ValidUID and ValidPWD, are used throughout the authentication routines in this article's code download.

 

Private Function ValidUID(ByVal UserID As String) As Boolean

    Dim oReg As Regex

    ' \w means alphanumeric

    ' {1,15} means 1 to 15 characters

    ' ^ and $ mean to match from the beginning to the end of

    ' the string

    ' (i.e. not a partial match, but the whole string)

    oReg = New Regex("^\w{1,15}$")

    If Not oReg.IsMatch(UserID) Then

        ValidUID = False

     Else

        ValidUID = True

    End If

End Function

 

Private Function ValidPWD(ByVal Password As String) _

    As Boolean

    Dim oReg As Regex

    ' \S means ascii characters

    ' \d means numbers

    ' Thus, ValidPWD requires at least one number somewhere

    ' within the string

    

    oReg = New Regex("^(\S{0,14})(\d{1,15})(\S{0,14})$")

    If Not oReg.IsMatch(Password) Then

        ValidPWD = False

    Else

        ValidPWD = True

    End If

End Function

FIGURE 5: Use Regex objects to validate user input.

 

To use regular expressions, you specify a pattern and look for either full or partial matches within a string. In the case of the ValidUID routine, the match string is simple. The code is looking for a series of alphanumeric characters with a total length of no more than 15 characters. Regular expressions are incredibly powerful for specifying data patterns and well worth your while to take the time to learn.

 

Step 7. Authenticate the User Safely

Having validated user data, the next step is to attempt to authenticate the user. The exact code will vary depending on many factors, including where you decided to locate the authentication data. Your analysis in Step 3 should help you determine the correct location for your data. The two most common choices are to store the authentication in either the web.config file or in a database.

 

Regardless of the authentication method, the general process is the same. A hash is calculated for the password. The password hash is compared to the stored password hash. If the hashes match, an Authentication Ticket is created to hold the credentials. The user's request (along with the authentication ticket) is routed to the originally requested URL.

 

If you've decided to store the hash in the Web.config file, the entire authentication process is managed by the FormsAuthentication class as shown in FIGURE 6.

 

Private Sub btnLogin_Click(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles btnLogin.Click

  If ValidUID(txtUserID.Text) And _

   ValidPWD(txtPassword.Text) Then

      If FormsAuthentication.Authenticate(txtUserID.Text, _

       txtPassword.Text) Then

         lblUserMsg.Text = ""

         ' the "false" parameter indicates that we don't

         ' need a persistent cookie

         FormsAuthentication.RedirectFromLoginPage( _

          txtUserID.Text, False)

      Else

         lblUserMsg.Text = _

          "Unable to authenticate this userid/password"

      End If

  Else

      lblUserMsg.Text = _

       "Unable to authenticate this userid/password"

  End If

End Sub

FIGURE 6: The authentication process is managed by the FormsAuthentication class when you store the credentials in the web.config file.

 

The FormsAuthentication.Authenticate method used in FIGURE 6 performs the "hash and compare" operation and determines if the credentials are valid. If so, it the code then uses the FormsAuthentication.RedirectFromLoginPage method to route the user back to the originally requested page. (I'll discuss redirection options in more detail in Step 8.)

 

If your site requires frequent updates to authentication information or if it needs to synchronize authentication across multiple Web servers, you will probably want to store the authentication information in a database. It is crucial that you avoid the "string building" dynamic SQL seen in so many examples. Simply put, database interaction should occur through a combination of stored procedures, the command object, and its parameters collection. FIGURE 7 shows an example of safe database access.

 

Private Function IsAuthentic(ByVal UserID As String, _

 ByVal Pass As String) As Boolean

 

   Dim sPassHash As String

   Dim oConn As SqlClient.SqlConnection

   On Error GoTo ErrorHandler

 

   IsAuthentic = False

   ' Get the hashed value of the password

   sPassHash = FormsAuthentication. _

    HashPasswordForStoringInConfigFile( _

    Pass, "md5")

   oConn = New SqlConnection()

 

   ' IMPORTANT: This example uses NT authentication to

   ' secure the database further.

   oConn.ConnectionString = _

    "Data Source=breidenbach;Database=Northwind;" & _

    "Integrated Security=SSPI;"

   oConn.Open()

 

   ' Create a command object and give it the name

   ' of the stored procedure.

   Dim oCmd As New SqlCommand("AuthenticateUser", oConn)

   oCmd.CommandType = CommandType.StoredProcedure

 

   ' Add the UserID and Password parameters.

   oCmd.Parameters.Add("@UID", SqlDbType.VarChar, 15)

   oCmd.Parameters.Add("@PWD", SqlDbType.Char, 32)

   oCmd.Parameters("@UID").Value = UserID

   oCmd.Parameters("@PWD").Value = sPassHash

 

   ' Retrieve the result of the stored procedure.

   ' (1=authentic, 0=not authenticated)

   If iCount = 1 Then

      IsAuthentic = True

   End If

   oConn.Close()

Exit Function

 

ErrorHandler:

   ' Don't show the database error to the user because

   ' it might disclose information useful to an attacker.

   Err.Clear()

   IsAuthentic = False

End Function

FIGURE 7: Use a combination of stored procedures, the command object, and its parameters collection to create a secure database. The strong data typing and delineation between code and data more than justify the extra effort to use command parameters.

 

A final note on interacting with the user during this process: It is crucial that, when authentication fails, you be careful about what information you tell the user. In general, the error message should let them know that an authentication error occurred without giving a hacker enough information to start guessing the correct values. Usually, a generic "unable to match the user ID or password" message is sufficient.

 

Step 8. Create the Authentication Ticket

After authenticating the user's credentials, you must create an authentication ticket. The authentication ticket is a cookie containing the user's ID and credentials (I'll discuss cookieless authentication in Step 9.) The default behavior is for this cookie to expire 30 minutes after the last request or when the browser closes, whichever comes first. This behavior may be overridden programmatically like this:

 

' First parameter is user's name, second is

' whether to persist cookie.

TicketCookie = FormsAuthentication.GetAuthCookie( _

 sUser, True)

TicketCookie.Expires = DateTime.Now.AddMinutes(15)

 

As I noted in the table in FIGURE 1, you can also add a limited amount of user information to the authentication ticket. Most browsers allow a maximum of 4K per cookie, so you must use this capability sparingly. In the example shown in FIGURE 6, I have added the user's color preferences to the authentication ticket. It requires you to create an authentication ticket manually, then encrypt it and add it to the cookie collection. FIGURE 8 also shows how to persist an authentication ticket and use an expiration time.

 

Dim oTicket As FormsAuthenticationTicket

Dim EncryptedTicket As String

Dim TicketCookie As HttpCookie

 

sColor = GetFavoriteColor(sUser)

 

' First parameter is version number

' Second parameter is cookie name

' Third parameter is current date

' Fourth parameter is expiration time

' Fifth parameter is whether to persist the cookie

' Sixth parameter is the user's additional data

oTicket = New FormsAuthenticationTicket(1, _

 FormsAuthentication.FormsCookieName, _

 DateTime.Now, _

 DateTime.Now.AddMinutes(15), _

 True, _

 sColor)

 

EncryptedTicket = FormsAuthentication.Encrypt(oTicket)

TicketCookie = New _

 HttpCookie(FormsAuthentication.FormsCookieName, _

 EncryptedTicket)

Response.Cookies.Add(TicketCookie)

Response.Redirect(FormsAuthentication.GetRedirectUrl(sUser,True))

FIGURE 8: This Figure shows how to create an authentication ticket with user data, persistence, and expiration time. Note that you must also add the ticket to the Cookies collection programmatically.

 

At this point, you have authenticated the user and created an appropriate authentication ticket whose expiration and persistence properties are set to meet the requirements defined for the target installation environment. If you wanted user data included in the ticket, it has been added to the cookie as well. The last step in the process is to redirect the user and ticket correctly to the originally requested page.

 

Step 9. Redirect

There are three general redirection scenarios: simple FormsAuthentication redirection, redirection of a manually created FormsAuthenticationTicket, and cookieless redirection.

 

In a simple scenario, where you can assume cookies and have not created your own authentication ticket, redirection is straightforward. The FormsAuthentication class's RedirectFromLoginPage method routes the user and ticket to the originally requested Web page automatically. The first parameter identifies the user, and the second indicates whether the cookie should be persisted:

 

FormsAuthentication.RedirectFromLoginPage(sUser, False)

 

When redirecting a manually generated ticket, you need to add it to the Response.Cookies collection and redirect to the requested page:

 

Response.Cookies.Add(TicketCookie)

Response.Redirect(FormsAuthentication.GetRedirectUrl( _

 sUser, True))

 

The third, cookieless scenario, is a bit more complex. The ticket is presented to the requested page by encrypting it and adding it to the URL as the value associated with the cookie's name. (This is the name you assigned in the name attribute of web.config's element.)

 

The code necessary to implement authentication without cookies is shown in FIGURE 9. You use the FormsAuthentication.Encrypt method to encrypt the ticket. You must examine the requested URL to determine whether it contains parameters already; adjust the syntax for concatenating the encrypted value accordingly.

 

Dim oTicket As FormsAuthenticationTicket

Dim Encrypted As String

Dim TicketCookie As HttpCookie

 

oTicket = New FormsAuthenticationTicket(sUser, False, 30)

Encrypted = FormsAuthentication.Encrypt(oTicket)

CallingURL = FormsAuthentication.GetRedirectUrl(sUser, _

 False)

If CallingURL.IndexOf("?") = -1 Then

   CookielessURL = CallingURL & "?" & _

   FormsAuthentication.FormsCookieName & "=" & Encrypted

Else

   CookielessURL = CallingURL & "&" & _    

   FormsAuthentication.FormsCookieName & "=" & Encrypted

End If

Response.Redirect(CookielessURL)

FIGURE 9: This shows how to implement cookieless authentication. You will use FormsAuthentication.Encrypt to encrypt the ticket, followed by adding the result programmatically to the URL before redirecting to the requested URL.

 

Hopefully, this article has provided you with a better understanding of the key coding choices in forms authentication and how to assess them. To help you pull the pieces together even further, an example authentication Web application is available for download. It showcases each of the authentication choices I discussed in this article.

 

Beth Breidenbach is a product architect for Getronics, a Netherlands-based provider of software and infrastructure solutions throughout the world. A self-professed "data geek," Beth has an abiding interest in all aspects of data design, security, storage, and transmission - which was a natural lead-in to exploring the possibilities inherent in the new family of XML-related technologies. Beth's most recent project was the application of XML and database technologies to rule processing engines. E-mail Beth at mailto:[email protected].

 

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

Authentication vs. Authorization

It's important to understand the distinction between authentication and authorization. Authentication is the process of verifying that the credentials presented by the user are valid. Authorization is the determination of what functions, data, and resources the user should be allowed to access. Thus, authorization always occurs after authentication.

 

This article does not attempt to address authorization, as it is another subject in and of itself.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish