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

Professional ASP.NET Security - Jeff Ferguson

.pdf
Скачиваний:
28
Добавлен:
24.05.2014
Размер:
13.26 Mб
Скачать

//set the expiry date of the authentication cookie authenticationCookie. Expires = DateTime.Now.AddDays (10)

//add the cookie to the HTTP response Response. Cookies. Add (authenticationCookie) ;

//redirect the user back to their original request Response. Redirect (FormsAuthentication.GetRedirectUrl (UsernameTextBox.Text , true) ) ;

}

else

{

//make the error message visible ErrorMessageLabel .Visible = true;

The code for checking the credentials is exactly the same. The changes are in the section that executes if the credentials match. First we use GetAuthCookie to create a new instance of HttpCookie:

HttpCookie authenticationCookie =

FormsAuthentication. GetAuthCookie (UsernameTextBox.Text, true) ;

We then get the current date and time by using the DateTime .Now static property, add 10 days to it using the DateTime . AddDays method, and use this as the expiry date and time of the cookie:

authenticationCookie. Expires = DateTime.Now.AddDays

(10) ; We have to add the cookie to the HTTP response ourselves:

Response. Cookies. Add (authenticationCookie) ;

Finally, we redirect the user back to their original request, which we obtain by using the GetRedirectUrl method of FormsAuthentication:

Response. Redirect (FormsAuthentication. GetRedirectUrl

(UsernameTextBox.Text , true) ) ;

The end result is a cookie that will persist beyond the closing of the browser, but which will expire after 10 days, forcing the user to enter their credentials again.

Using Other Credentials Stores

Up to this point, we have been using the web. conf ig file to store our users' credentials. While this is OK for demonstrating forms authentication or for applications that will only have a few users, storing credentials in the web. conf ig, is not a very manageable approach for applications that need to have users added and removed regularly. The web. conf ig does not support the addition of any information to the usernames and passwords that it stores so we cannot, for example, store a user's e-mail address along with their credentials.

188

Forms Authentication

Fortunately, we can use whatever credentials store we like with forms authentication. We just have to write our own code for checking the credentials of users when they log in and use that in place of the

FormsAuthentication. Authenticate method we have been using up until now.

An Interface for Credentials Stores

The pieces of code we will be creating for checking credentials against various stores will all perform the same function: they will check a username and password against the details in the store and return a Boolean result. Since they perform the same function, it would be nice if we could switch between credentials stores easily. This would be good for flexibility and reusability of our code. For example, if we start a web site with a small number of users, an XML file on the server might be sufficient but, as the site grows, we may need to move to using a database server to store user details. If the code for accessing the credentials stores is implemented in a consistent way, this changeover will be easy.

A good way of allowing code like this to be switched is to define an interface for the shared functionality and put the code into classes that implement the interface. This is what we will do: we will create a simple interface for credentials stores and implement it in a number of classes that each support the use of a different store. Here is the code that defines our (very simple) interface:

using System;

namespace credentialsStores {

public interface ICredentialsStore

{

bool Authenticate (string username, string password);

So, every class that implements credentialsStores . ICredentialsStore must provide an Authenticate method that takes a username and password and returns a Boolean result.

Before we implement ICredentialsStore for other credentials stores, let's create an implementation that checks credentials against the web. conf ig. This will allow us to use the default credentials store interchangeably with the other stores we will create.

The implementation is very simple:

using System;

using System. Web. Security;

namespace credentialsStores {

public class DefaultCredentialsStore : ICredentialsStore { public bool Authenticate (string username, string password) {

return FormsAuthentication. Authenticate (username, password);

189

We just use the Authenticate method required by ICredentialsStore to 'wrap' the Authenticate method of FormsAuthentication.

To slot this into the basic forms authentication example we covered earlier, we just need to change a couple of lines in the method that we use for logging the user in:

private void LoginButton_Click(object sender, System. EventArgs e) { //create a credentials store

ICredentialsStore credentialsStore = new DefaultCredentialsStore ( ) ;

//check the credentials

if (credentialsS tore. Authenticate(UsernameTextBox. Text, PasswordTextBox. Value) ) {

//set up the authentication cookie and redirect

FormsAuthentication. RedirectFromLoginPage (UsernameTextBox.Text, false) ; } else

{

//make the error message visible ErrorMessageLabel. Visible = true;

We now create an ICredentialsStore object using the DefaultCredentialsStore constructor and call the Authenticate method of that object to check the credentials.

We could have created a credentials store interface that required a lot more functionality, such as the ability to add users to the store or change the details of a user. This would be really useful as credentials stores could be switched without having to hunt down the code for all these functions and change it. The interface we have used is a simple example of how thinking about flexibility can keep our security options open.

Storing Credentials in a Separate XML File

The web. conf ig file that is the default store for forms authentication credentials is an XML file but, as we mentioned earlier, it does not allow any additional tags or data to be added to it beyond what is included the standard ASP. NET settings schema. This means that we cannot use the web. conf ig to store information about users beyond their usernames and passwords.

It makes sense to keep information about users in the same place so; it makes sense to store their credentials in the same place as their other information. If we want to continue to use XML to store user credentials, we will need to create our own XML file for storing user detaik and check users' credentials against this file.

The Format of the XML Users File

We could use any schema we like for our XML users file - that is the advantage of using our own file rather than the web. conf ig. For this example, we will be using a file structured like this one:

190

Forms Authentication

<users passwordFormat="MD5" >

<user username="dan" password="F3277F51A7273361E85F31EBD542D608 "> </user>

<user username=" jenny" password="431D76CA439EA33F17E3CEF99CB982E3 "> </user> </users>

As you can see, we are using hashed passwords to protect our credentials from theft. We use the passwordFormat attribute of the <users> element in exactly the same way as that of the <credentials> element in the web. conf ig. We don't have to do this - we are just following the convention of using this attribute so that our system will be easy to understand.

Unlike the web . conf ig, the format of our file is flexible. We can add additional information for users inside <user> tags. We could for example, include e-mail addresses for users that we have these details for:

<users

passwordFormat="MD5">

<user username="dan"

password="F3277F51A7273361E85F31EBD542D608">

<email>danielk@wrox. com</email>

</user>

 

<user

username=" jenny"

password="431D76CA439EA33F17E3CEF99CB982E3 ">

</user> </users>

Now that we have defined how our XML users file will be formatted, we can move on to create a class that will check credentials against it.

Here is the completed class:

public class XMLCredentialsStore : ICredentialsStore { private string UsersFile;

public XMLCredentialsStore (string usersFile) { UsersFile = usersFile;

public bool Authenticate (string username, string password)

//create the xml document object XmlDocument usersXml = new XmlDocument ( ) ;

//create a namespace manager for the document (we need it later) XmlNamespaceManager namespaceManager = new XmlNamespaceManager (usersXml. NameTable) ;

//the xml document might fail to load so we use a try/catch try

1Q1

usersXml.Load(UsersFile) ; } catch (Exception error) {

//we could not load the xml file so we cannot authenticate the user return false;

//get the users node

XmlNode users = usersXml .GetElementsByTagName ( "users" ). Item(O) ;

string hashingAlgorithm = users .Attributes [ "passwordFormat" ] .Value; string passwordToCompare;

//if a hashing algorith is specified, hash the password

if ( (hashingAlgorithm != null) && (hashingAlgorithm != "Clear")) passwordToCompare =

FormsAuthentication.HashPasswordForStoringlnConf igFile (password, hashingAlgorithm) ; else

passwordToCompare = password;

//get the root node

XmlNode root = usersXml . DocumentElement ;

//create an XPath expression to match a user node with the correct username //and password

//NOTE: We may need to protect against XPath code injection! string userXPath = "descendant :: user [@username= '" + username +

" ' and @password= ' " + passwordToCompare + " ' ] " ;

//find a matching user node

XmlNode matchingUser = root . SelectSingleNode (userXPath, namespaceManager );

if (matchingUser != null) return true;

else

return false;

We start by indicating that this class implements ICredentialsStore:

public class XMLCredentialsStore : ICredentialsStore We also define a simple constructor that initializes the location of the XML file to use:

private string UsersFile;

public XMLCredentialsStore (string usersFile) { UsersFile = usersFile;

192

Forms Authentication

We now move on to implementing the Authenticate method. We start by creating an XmlDocument object and loading the XML users file into it. We also create an XmlNamespaceManager as we will need this later when we use an XPath expression to find a matching user element.

XmlDocument usersXml = new XmlDocument ( ) ;

//create a namespace manager for the document (we need it later) XmlNamespaceManager namespaceManager = new XmlNamespaceManager (usersXml . NameTable ) ;

//the xml document might fail to load so we use a try/catch try

{

usersXml .Load(UsersFile) ; }

catch (Exception error) {

//we could not load the xml file so we cannot authenticate the user return false;

If an error occurs while we are loading the XML file, we return false to indicate that the user could not be authenticated. This is a safe option for security. This follows the general rule that the default behavior of the system should always be secure. Another option would have been to bubble the exception up to the object that called Authenticate and let that deal with error handling.

Our next step is to locate the <users> node in the XML file:

XmlNode users = usersXml .GetElementsByTagName ( "users" ). Item(O) ;

We need to extract the passwordFormat attribute from this tag so that we can decide whether we need to apply a hashing algorithm when checking the password against the XML file.

string hashingAlgorithm = users .Attributes [ "passwordFormat "] .Value;

We set up a new string variable, passwordToCompare, to hold the password value we will actually compare to those stored in the web. conf ig. If the hashing algorithm specified in the passwordFormat attribute was not nothing or "Clear", we use the specified algorithm to hash the inputted password through the HashPasswordForStoringlnConf igFile method.

string passwordToCompare;

if ( (hashingAlgorithm != null) && (hashingAlgorithm != "Clear")) passwordToCompare =

FormsAuthentication . HashPasswordForStoringlnConf igFile (password, hashingAlgorithm) ; else passwordToCompare = password;

Otherwise we just use the original value of the password for passwordToCompare.

t no

We now need to search the XML document for a <user> element that has the correct username and password attributes. To do this, we will use an XPath expression with this form:

descendant::user[@username='username' and @password='password']

We need to apply this expression to an XmlNode so we first obtain the root node of our XML document with the DocumentElement property:

XmlNode root = usersXml.DocumentElement;

Next, we build our XPath expression, using the username and passwordToCompare values. Remember, if a hashing algorithm is specified in the XML file, passwordToCompare will contain the hashed version of the password that was entered. If not, it will contain the password as is was entered.

string userXPath = "descendant::user[@username='" + username + "' and @password='" + passwordToCompare + "']";

Something worth noting here is that we are inserting values entered by the user into an XPath expression. It would be a good idea to harden our system against malicious users inserting carefully chosen usernames and passwords in order to alter the way our XPath expression works. If we do not take such precautions and a user enters:

"dan1 or password='"

For their username, the XPath expression will be subverted and will match any node when the username is "dan". This is obviously not desirable.

In Chapter 2, we looked at the problems of script injection and SQL injection. XPath injection can be protected against in the same way - by filtering, encoding, or validating what the user enters into our system. To protect against XPath injection, we probably want to prevent spaces, quotation marks, and equals signs making it through to our XPath expression.

Once we have created our XPath expression, it is a simple matter to find a node in the document that matches it:

XmlNode matchingUser = root.SelectSingleNode(userXPath,namespaceManager);

We then return true from the Authenticate method if a matching node was found and false if no matching node was found:

if(matchingUser != null) return true;

else

return false;

We now have an XML implementaion of ICredentialsStore that we can use in the same way as the Def aultCredentialsStore object that we created earlier.

194

Forms Authentication

To slot in our XML credentials store, we just need to create the XML file and change one line in the login method:

private void LoginButton_Click(object sender. System.EventArgs e) { //create a credentials store

ICredentialsStore credentialsStore = new XMLCredentiaisStore(Server.MapPath("Users.xml"));

//check the credentials if(credentialsStore.Authenticate(UsernameTextBox.Text, PasswordTextBox.Value))

{

//set up the authentication cookie and redirect FormsAuthentication.RedirectFromLoginPage(UsernameTextBox.Text,false); }

else

{

//make the error message visible ErrorMessageLabel.Visible = true;

Storing Credentials in a Database

If our application will have a large number of users, a relational database such as SQL Server is a good place to store the details of our users. A database server provides good scalability because we can share a database of users between several web servers in a web farm. Database servers can comfortably deal with a large number of users' details without significant performance problems and are designed to handle multiple concurrent requests.

We will create the class the implements the database credentials store in a similar way to the class for an XML store.

The database we will use has a tblUsers table that contains username and password columns.

Here is the code for the class:

using System;

using System.Data.OleDb;

namespace credentialsStores {

/ / / <surnmary>

///Summary description for DatabaseCredentialsStore.

///</summary>

public class DatabaseCredentialsStore : ICredentialsStore

{

OleDbConnection Connection;

1QK

public DatabaseCredentialsStore(01eDbConnection connection) { Connection = connection; }

public bool Authenticate(string username, string password) { bool isAuthenticated = false;

try

{

//open the connection Connection.Open();

//*****we should use a stored procedure to prevent SQL injection string selectSql = "SELECT password FROM tblUsers WHERE username ='' username + "' AND password='" + password + "'";

OleDbCommand selectCommand = new OleDbCommand(selectSql,Connection); OleDbDataReader matchingUser = selectCommand.ExecuteReader();

//default implementation of SELECT is not case sensitive //so we make sure... if(matchingUser.Read())

if((string)matchingUser["Password"] == password) isAuthenticated = true;

matchingUser.Close();

Connection.Closet);

}

catch(Exception error)

{

return false;

return isAuthenticated; } } }

NOTE: We have used a SQL query directly in our code here, in order to keep the code in one place for this demonstration. In practice, we should use a stored procedure to do the comparison. This has advantages for performance but more importantly, helps guard against SQL injection. In Chapter 2, we looked at how to prevent SQL injection. The techniques we used there should be used for systems like this f>ne in the real world.

Our constructor simply takes an OleDbConnection object and stores it for later use:

OleDbConnection Connection;

public DatabaseCredentialsStore(OleDbConnection connection)

196

Forms Authentication

Connection = connection; }

This allows the code that uses our class to specify which database to use and how to access it.

Inside the Authenticate method, we set up a Boolean variable to store whether the user was authenticated. We the open a try block (because our database connection may fail) and open the connection:

bool isAuthenticated = false; try

{

//open the connection Connection.Open();

Our next step is to define the SQL SELECT command that will attempt to find a matching user in the database and create an OleDbCommand object for it:

string selectSgl

= "SELECT

password FROM

tblUsers WHERE username = ' "

+

username + "'

AND password='"

+

password

+ "'";

 

OleDbCommand selectCommand

= new OleDbCommand(selectSql,Connection);

We now execute the command we have built and store the results in a DataReader:

OleDbDataReader matchingUser =

selectCommand.ExecuteReader( ) ;

Although the DataReader contains only users whose username and password were matched by the SQL query, we need to check the password once more. The reason for this is that many database servers perform a case-insensitive match when they use SELECT. For example, the default behavior of SELECT in SQL Server is not case sensitive! We definitely want our passwords to be case sensitive so we compare the password ourselves:

if(matchingUser.Read()) if((string)matchingUser["Password"] == password)

isAuthenticated = true;

Note: The fact that SELECT can be case insensitive by default is often forgotten. In SQL

Server, the behavior of matching and sorting is determined by the collation that is active for the column involved. The default collation for text columns is case insensitive. It is, therefore, wise to take extra precautions in our code.

The final part of this method closes the data objects we have been using, deals with errors by returning a false value, and returns the Boolean isAuthenticated value:

matchingUser.Close();

Connection.Close();

}

catch(Exception error)

{

return false; }

return isAuthenticated;

197

Соседние файлы в предмете Программирование