menu
  index  ==>  papers  ==>  web  ==>  asp_net  ==>  asp_net_20_users_roles_profiles   

Asp.Net 2.0 Security : Users, Roles, Profiles - Felix John COLIBRI.


1 - Introduction

Many web sites must manage the Users which read the web pages: only clearly identified Users should be allowed to access certain resources.

The User management tasks are quite obvious for message boards, social network sites, web portals, or eCommerce.

The site management must include user accounts, and ways to authenticate the incoming User (check who he really is) and define which resources he should be allowed to get access to.

Asp.Net 1.x already had defined how to authenticate and authorize Users. But all the User management, the Login interface, the storing of those informations had to be coded by the developer.



Asp.Net 2.0 takes a big step forward by including

  • User management
  • the specification of Roles (groups of Users for authorization purposes)
  • handling of Profiles (personal User properties)
And we have
  • dedicated web controls for handling the most obvious tasks (Login, changing the password etc)
  • default behaviour specified by parameter files
  • programmatic support for handling more specific cases


We first will review the security basics, then will move along to present Asp.Net 2.0 security services.




2 - Basic Asp.Net 1.x Security

2.1 - Authentication

2.1.1 - Authentication Steps

To add authentication, the steps are the following:
  • in all the pages for which we want to check the user credentials, including eventually the Default.Aspx, we check whether the User is authenticated. This is usually performed in OnPageLoad:

    procedure TDefault.Page_Load(senderSystem.Object;
        eSystem.EventArgs);
      begin
        if not User.Identity.IsAuthenticated
          then  Response.Redirect('LoginForm.aspx')
          else Label1.Text:= 'Welcome: 'User.Identity.Name;
      end// Page_Load

  • in the Web.Config file, we add two information
    • we want to use Forms Authentication (as opposed to "Windows", "Passport", or any other)
    • we want to redirect any user to the login form that we specify
    Here is an example:

     
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <system.web>
        <authentication mode="Forms">
          <forms
              name="my_login_cookie"
              loginUrl="LoginForm.aspx"
              timeout="3">
          </forms>
        </authentication>
      </system.web>
    </configuration>

  • the login form itself, LoginForm.Aspx asks the User to enter his credentials (name and password in our case), and decide, based on some logic, whether this user is considered authenticated

    function TLoginForm.f_are_user_and_pass_valid(
        p_user_namep_passwordString): Boolean;
      begin
        Result:= (p_user_name = 'my_user'and (p_password = 'my_pass');
      end// f_are_user_and_pass_valid

    procedure TLoginForm.login__Click(senderTObject;
        eSystem.EventArgs);
      begin
        if f_are_user_and_pass_valid(
            user_textbox_.Textpassword_textbox_.Text)
          then FormsAuthentication.RedirectFromLoginPage(
              user_textbox_.Text,  False);
      end// login__Click

    Where

    • the function f_are_user_and_pass_valid decides, based on hard coded string litteral, whether some user name and password are correct or not
    • this function is called by the LoginButton.OnClick, and if the result is True, the RedirectFromLoginPage send to the User the page he first asked for (Default in our case)


2.1.2 - Authentication Logic

The authentication logic looks like this:

authentication_logic



2.1.3 - Tracing the Authentication Steps

Here is the scenario of a non-authenticated user requesting Default.Aspx, traced using our
Cassini Spy tracing tool:
  • Cassini (or any other HTTP Server is started)

    cassini_manual_start

  • using Internet Explorer (or any suitable Web Browser), the user requests a page from our site. For instance, Default.Aspx

    start_internet_explorer

    When the User sends his request, the HTTP request is sent to the Server (partial trace)

     
    -> | GET /p_92_forms_authentication/Default.Aspx HTTP/1.1
    -> | Accept: image/gif, image/x-xbitmap, ...ooo...
    -> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; ...ooo...
    -> | Host: localhost:81
    -> | Connection: Keep-Alive
    -> |
    -> |

  • the server loads Default, User.Identity.IsAuthenticated returns False, the Server returns an error 302, asking the Browser to redirect to Login.Aspx

    <- | HTTP/1.1 302 Found
    <- | Server: Cassini/1.0.0.0
    <- | Date: Sun, 04 Nov 2007 06:45:03 GMT
    <- | X-AspNet-Version: 1.1.4322
    <- | Location: /p_92_forms_authentication/LoginForm.Aspx
    <- | Set-Cookie: ASP.NET_SessionId=tvol0y45oij1do45l0opot45; path=/
    <- | Cache-Control: private
    <- | Content-Type: text/html; charset=utf-8
    <- | Content-Length: 158
    <- | Connection: Close
    <- |
    <- |
    <- | <html><head><title>Object moved</title></head><body>
    <- | <h2>Object moved to <a href='/p_92_forms_authentication/
           LoginForm.Aspx'>here</a>.</h2>
    <- | </body></html>
    <- |
     

  • the browser requests the redirected page (we hardly see this in the Browser):

     
    -> | GET /p_92_forms_authentication/LoginForm.Aspx HTTP/1.1
    -> | Accept: image/gif, image/x-xbitmap, image/jpeg, ...ooo...
    -> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; ...ooo...
    -> | Host: localhost:81
    -> | Connection: Keep-Alive
    -> | Cookie: ASP.NET_SessionId=tvol0y45oij1do45l0opot45
    -> |
    -> |

  • the Server sends back the LoginForm.Aspx:

     
    <- | HTTP/1.1 200 OK
    <- | Server: Cassini/1.0.0.0
    <- | Date: Sun, 04 Nov 2007 06:45:03 GMT
    <- | X-AspNet-Version: 1.1.4322
    <- | Cache-Control: private
    <- | Content-Type: text/html; charset=utf-8
    <- | Content-Length: 974
    <- | Connection: Close
    <- |
    <- |
    <- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <- | <html>
    <- |   <head>
    <- |     <title></title>
    <- |   </head>
    <- |   <body ms_positioning="GridLayout">
    <- |     <form name="_ctl0" method="post"
                     action="LoginForm.Aspx" id="_ctl0">
    <- |       <input type="hidden" name="__VIEWSTATE"
                     value="dDw3ODA0Mj ...ooo... U6r" />
    <- |       <span id="Label1" style="Z-INDEX: 3; ...ooo... />
    <- |       <input name="user_textbox_" type="text"
                     id="user_textbox_" style=" ...ooo... />
    <- |       <span id="Label2" style="Z-INDEX: ...ooo... />
    <- |       <input name="password_textbox_" type="text"
                     id="password_textbox_" style=" ...ooo... />
    <- |       <input type="submit" name="login_button_" value="Login"
                     id="login_button_" style=" ...ooo... />
    <- |     </form>
    <- |   </body>
    <- | </html>

    which is displayed like this:

    login_form

  • the user type his user name "my_user" and his password "my_pass" and click "Login"

     
    -> | POST /p_92_forms_authentication/LoginForm.Aspx HTTP/1.1
    -> | Accept: image/gif, image/x-xbitmap, image/jpeg ...ooo...
    -> | Referer: http://localhost:81/p_92_forms_authentication/
                LoginForm.Aspx
    -> | Content-Type: application/x-www-form-urlencoded
    -> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; ...ooo...
    -> | Host: localhost:81
    -> | Content-Length: 124
    -> | Connection: Keep-Alive
    -> | Cache-Control: no-cache
    -> | Cookie: ASP.NET_SessionId=tvol0y45oij1do45l0opot45
    -> |
    -> | __VIEWSTATE=dDw3ODA0MjA4 ...ooo...
                4BNmJU6r&user_textbox_=my_user
                &password_textbox_=my_pass&login_button_=Login

    which contains the credentials (non crypted because we did not ask for it)

  • the Server asks the Browser to redirect to Default.Aspx

     
    <- | HTTP/1.1 302 Found
    <- | Server: Cassini/1.0.0.0
    <- | Date: Sun, 04 Nov 2007 06:45:11 GMT
    <- | X-AspNet-Version: 1.1.4322
    <- | Location: /p_92_forms_authentication/default.aspx
    <- | Set-Cookie: logincookie=FBD8A89 ...ooo...0765F4A667; path=/
    <- | Cache-Control: private
    <- | Content-Type: text/html; charset=utf-8
    <- | Content-Length: 1162
    <- | Connection: Close
    <- |
    <- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <- | <html>
    <- |   <head>
    <- |     <title></title>
    <- |   </head>
    <- |   <body ms_positioning="GridLayout">
    <- |     <form name="_ctl0" method="post"
                     action="LoginForm.Aspx" id="_ctl0">
                      ...ooo... same as previous LoginForm
                      ...ooo... but with User and Pass filled
    <- |     </form>
    <- |   </body>
    <- | </html>

  • since this is a redirection request, the Browser asks for the redirected page:

     
    -> | GET /p_92_forms_authentication/default.aspx HTTP/1.1
    -> | Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*
    -> | Referer: http://localhost:81/p_92_forms_authentication/LoginForm.Aspx
    -> | User-Agent: Mozilla/4.0 (compatible; MSIE ...ooo...
    -> | Host: localhost:81
    -> | Connection: Keep-Alive
    -> | Cache-Control: no-cache
    -> | Cookie: ASP.NET_SessionId=tvol0y45oij1do45l0opot45;
                     logincookie=FBD8A8940 ...ooo...
    -> |
    -> |

  • the Server returns, at long last, the Default.Aspx page:

     
    <- | HTTP/1.1 200 OK
    <- | Server: Cassini/1.0.0.0
    <- | Date: Sun, 04 Nov 2007 06:45:11 GMT
    <- | X-AspNet-Version: 1.1.4322
    <- | Cache-Control: private
    <- | Content-Type: text/html; charset=utf-8
    <- | Content-Length: 490
    <- | Connection: Close
    <- |
    <- |
    <- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <- | <html>
    <- |   <head>
    <- |     <title></title>
    <- |   </head>
    <- |     <body>
    <- |       <form name="_ctl0" method="post" action="default.aspx" id="_ctl0">
    <- |         <input type="hidden" name="__VIEWSTATE"
                     value="dDwyMDQw ...ooo...COAfH2XUVkKIvVc=" />
    <- |
    <- |         <span id="Label1">Welcome: my_user</span>
    <- |       </form>
    <- |     </body>
    <- | </html>

    and the user sees:

    page_returned



2.1.4 - UML CLASS Diagrams

We can sum-up the CLASSes we used with the following Class diagram:

security_uml_class_diagram



2.2 - Authorization

Once your site has checked that the user really is the person he claims to be, you can decide which resource (page, folder, file etc) he is allowed to use. This is called Authorization.

For folder authorization, we can be set up using Web.Config:

  • for each folder we want to protect, we create the <authorization section and write a combination of <allow and <deny tags
  • if the user is not allowed to access some resource, he is sent back to the login page
The rules to allow or deny are the following
  • the rules are analyzed in sequential order, and the first which is true exits (the rules after this one are not checked)
  • the two wildcards are
    • ? for ANONYMOUS
    • * for ALL


Here is an example:
   we add the following section to Web.Config (in the root page):

 
<configuration>
  <system.web>
    <authentication mode="Forms">
      <forms
          name="my_login_cookie"
          loginUrl="LoginForm.aspx"
          timeout="3">
        <credentials passwordFormat= "Clear">
          <user name="my_user" password="my_pass" />
          <user name="joe" password="joe_pass" />
          <user name="ann" password="ann_pass" />
        </credentials>
      </forms>
    </authentication>
    <authorization>
      <deny users="?" />
      <allow users="my_user" />
      <allow users="my_user,joe,ann" />
      <deny users="ann" />
    </authorization>
  </system.web>
</configuration>

   we create a my_login.aspx page, which contains 2 Textboxes and one Button, which checks the user and pass:

procedure TLoginForm.login__Click(senderTObject
    eSystem.EventArgs);
  begin
    if FormsAuthentication.Authenticate(user_textbox_.Text
          password_textbox_.Text)
      then FormsAuthentication.RedirectFromLoginPage(
          user_textbox_.Text,  False);
  end// login__Click

   all pages are now protected by the Asp.Net checks, and there is no need to write explicit checks. For instance, in the Default.Page_Load, we have:

procedure TDefault.Page_Load(senderSystem.Object
    eSystem.EventArgs);
  begin
    Label1.Text:= 'Welcome: 'User.Identity.Name;
  end// Page_Load

   when we run the project, the login is presented. Only MY_USER and JOE will see the Default.Aspx,


Please note:
  • authentication can ONLY be places in the root Web.Config, but authorization can be placed in any folder of our site
  • the test User.Identity.IsAuthenticated in each page is no longer necessary
  • we could hash the password in Web.Config, to avoid people on the Server having access to Web.Config to see those passes
  • we could write code to place the user, password, authorization in some database, instead of placing them in Web.Config



3 - Membership and User Management

3.1 - Asp.Net 2.0 Membership Services

User management is about handling user credential (user-name, password, and maybe other items, like e-mail, preferences etc). It involves
  • creating, updating, deleting user accounts
  • enabling the developer to include visual controls for doing so.
Asp.Net 2.0 offers
  • predefined controls for Login, Logout, registration etc. So we no longer have to create forms with the user name and password TextBoxes and "Login" button
  • a pre-defined API for managing Users, which includes
    • a set of CLASSes
    • an implementation of the persistence of the users (saving the information on disc)


3.2 - Initializing Membership

When Membership in place, access to the pages will be protected by User / Passwords. This information is usually stored on disc.

So someone has to initialize the store, at least with a first User / Pass.

For Delphi, by default, the data is stored in a Blackfish database. So, initializing this data amounts to adding some rows to the user table. This can be done

  • using the Data Explorer
  • using a Delphi project (Win32, Vcl.Net or Asp.Net)


3.2.1 - Creating the Membership database

This Database is created automatically by Delphi when you run any Asp.Net application once.

Let's create this project:
   select "File | New | Asp.Net Web Application", specify the path and app name, like "membership_admin"
   run the project once
   the disc will contain a new APP_DATA path and the .jds database:

membership_database



We can examine this database using the Data Explorer:
   select "Data Explorer | dbExpress | BLACKFISHSQL | right click | Add New Connection"
   enter a connection name, like "bfs_asp_admin" click "Ok"
   a new node is created in the treeview
   select "bfs_asp_admin | Modify Connection", and enter the host and .jds path:

create_dbexpress_connection

test the connection and click "Ok"

   select the bfs_asp_admin node, and expand the node to display the database schema:

membership_schema



3.2.2 - Creating the Database with ASPNET_REGDB.EXE

The Membership database is created automatically when we run any Asp.Net project once.

Delphi simply creates the APP_DATA path, creates a new empty .JDS database, and initializes the empty TABLEs. This TABLE creation is performed by running a Delphi .EXE which is placed by the Delphi installation in the .Net Framework directory:

aspnet_regdb



You may notice that there is an ASPNET_REGSQL.EXE in the same directory, and this one if from Microsoft, and it will create Sql Server tables.



If your Delphi version does not create the bsql_aspnet.jds database, you can create the database using any technique (the Data Explorer, or any Win32 or Vcl.Net code).

Here is an example where we will create the same database in a temporary folder MY_APP_DATA
   select "Data Explorer | dbExpress | BLACKFISHSQL | right click | Add New Connection"
   enter a connection name, like "bfs_membership_base" click "Ok"
   a new node is created in the treeview
   select "bfs_asp_admin | Modify Connection", and enter the host and .jds path, like:
  • LocalHost
  • C:\programs\us\web\asp_net\membership_roles_profile\ 02_membership\21_membership_admin\my_app_data\bfs_membership.jds
   click "Advanced" and toggle "Create" to True
   click "Ok"
   click "Test Connection"
   an empty Blackfish database is created
   click "Ok" to save the new connection


And here is how to create the empty Membership TABLEs in this database:
   open Notepad, and ask aspnet_regdb to display its commands in a command-line box:

 
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regdb.exe /?

   save this under aspnet_regdb_help.bat (is is in the downloadable .ZIP) and run it
   here are the aspnet_regdb commands:

aspnet_regdb_commands

   since this help talks about MACHINE.CONFIG, here is this file's location:

machine_config

and this file, after Delphi installation, contains a Blackfish entry, with the BlackfishSQLAspDB connection name:

 
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  ...ooo...
  <connectionStrings>
    <add name="BlackfishSQLAspDB" connectionString="host "...ooo... />
    <add name="LocalSqlServer" connectionString="data " ..ooo... />
  </connectionStrings>
  
  ...ooo...
</configuration>



Now we can create the Membership TABLEs in this database:
   open Notepad, and type the line which will execute aspnet_regdb in order do create the empty TABLEs (Ed: the command on a single line):

 
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regdb.exe
    bfs_membership_base -A
pause

Save this under a .BAT name and start the .BAT

   the table is created

membership_base_initialized

And the .Jds size jumped from 52 K to 272 k



Instead of using the name of our connection ("bfs_membership_base") we could have used the MACHINE.CONFIG name, which is BlackfishSQLAspDB. In fact this is also the name of a default Ado.Net provider connection (Ed:: partial)

balckfish_sql_asp_db

Also note that the database is hardcoded, and still works for any of our Asp.Net project. This works because the database name uses the |DataDirectotry| "macro", which stands for "the App_Data subdirectory of the current project".



3.2.3 - Creating a Database and User manually

Finally, we could as well create the TABLEs ourselves. For the Sql Server engine, Microsoft provided the scripts:

provider_sql_scripts

But for Delphi, we have to extract them from the current table. You can either use a script extractor (we published several of those), or use the Dbx4 metadata facilities, or, for simple projects like this, simply look at the Data Explorer description:
   select "Data Explorer | dbExpress | BLACKFISHSQL | bfs_asp_admin | Tables | ASPNET_USERS | Alter Table"
   the schema is displayed

user_table_schema



Base on this view, an adequate script would be:

 
CREATE TABLE aspnet_users
  (
    applicationid CHAR(36) NOT NULL,
    usreid CHAR(36) NOT NULL,
    username VARCHAR(256) NOT NULL PRIMARY KEY,
    loweredusername VARCHAR(256) NOT NULL,
    mobliealias VARCHAR(16),
    isanonymous boolean NOT NULL,
    lastactivitydata TIMESTAMP NOT NULL
  )



Using this script, we can create the ASPNET_USERS Table by several means:

  • using the Data Explorer Active Query Builder (see our Dbx4 Programming paper)
  • using code from any Delphi application
Since we are in Internet country, we will use an Asp.Net application, with the DataStore connection and command:
   on the previously create blank Asp.Net application, select "Design", and in the Object Inspector, select "DOCUMENT" and then set PageLayout to GridLayout
   make sure the "Project Manager | References" contains the following assemblies (Ed: for this step and the other steps presented later):
  Borland.Data.DbxCommon.Dll
  Borland.Data.CommonDriver.Dll
  Borland.Data.AdoDbxClient.Dll
  Borland.Data.BlackfishSql.LocalClient.Dll
and if not, add them ("right click | Add reference" etc)
   select "Tools Palette | BlackfishSQL | DataStoreConnection", drag it on the Form and select "Object Inspector | ConnectionString | Ellipsis", and in the dialog, select the "bfs_membership_manual.jds" database:

datastore_connectionstring

test it and close

   select "Tools Palette | BlackfishSQL | DataStoreCommand", drag it on the Form and set its Connection to DataStoreConnection1
   drag a Button on the Form, and in its Click event, create the ASPNET_USERS Table:

const k_create_aspnet_users_request=
          'CREATE TABLE aspnet_users'
        + '  ('
        + '      applicationid CHAR(36) NOT NULL,'
        + '      usreid CHAR(36) NOT NULL,'
        + '      username VARCHAR(256) NOT NULL PRIMARY KEY,'
        + '      loweredusername VARCHAR(256) NOT NULL,'
        + '      mobliealias VARCHAR(16),'
        + '      isanonymous BOOLEAN NOT NULL,'
        + '      lastactivitydata TIMESTAMP NOT NULL'
        + '    )' ;

procedure execute_datastore_command(p_c_datastore_commandDataStoreCommand;
    p_requestString);
    // -- assumes the connection is open
  begin
    with p_c_datastore_command do
    begin
      CommandText:= p_request;
      ExecuteNonQuery;
    end// with p_c_datastore_command
  end// execute_datastore_command

procedure TDefault.create_user_table__Click(senderSystem.Object;
    eSystem.EventArgs);
  begin
    DataStoreConnection1.Open;
    execute_datastore_command(DataStoreCommand1,
        k_create_aspnet_users_request);
    DataStoreConnection1.Close;
  end// create_user_table__Click

   your USES clause should contain the following UNITs (Ed: for this and for the next steps below):
  System.Data.Common
  Borland.Data.DataStore
  Borland.Data.AdoDbxClientProvider;

   run and click on the Button
   the Table is created

If you select the Data Explorer, and refresh the "bfs_membership_manual" connection, you will also see the Table



For the rest of the presentation, we will use the App_Data database.



3.2.4 - The Delphi Security Tables

Before using the user Table, or any other Table, we must understand the relationships between those Tables. Here is a simplified schema of the database:

security_schema



And

  • the ASPNET_APPLICATIONS is there to allow the same database to be used by several Asp.Net applications. This can be later used by using the user PLUS the application for deciding what to authorize or deny
  • the ASPNET_USERS mainly contains the User name
  • in a separate Table, the ASPNET_MEMBERSHIP contains the User password, with lots of similar informations (the view above is truncated for this Table)
If we want to group users in Roles, we add the role management Tables:
  • ASPNET_ROLES define the role name
  • ASPNET_USERINROLES allow to link users in roles, in a typical n to n fashion (a user can have many roles, and a role obviously contains many users)
In addition, we can add user Profiles which mainly handle User preferences
  • for a given User, the ASPNET_PROFILE contains lists of properties (what the User likes)
There are other Tables which are less important and were not included in this schema.



Please note:

  • all ID's are 36 character strings. Looking at the Sql Server scripts, the type is "uniqueidentifier" and is exactly the size of a GUID
  • we will not use GUIDs which are quite cumbersome to handle, but use a simple ID scheme:
    • 100x for Applications
    • 200x for Users
    • 300x for Roles


3.3 - Creating the First User

3.3.1 - Entering new Users

Just to experiment how the Membership framework will work, we must have some userpass in our database.

There are several ways to do this:

  • using the Microsoft Asp.Net web administration pages
  • or directly inserting data in our database Tables
    • using the Data Explorer
    • by code


3.3.2 - The Microsoft Asp.Net Configuration application

Here is how to use the configuration utility:
   start the Asp.Net Application for which you want to enter users (remember, the database is located, by default in the APP_DATA subdirectory of each Asp.Net application)
   select "Project Manager | your_project.dll | right click Asp.Net_Configuration"
   the main page of the administration tool is displayed:

web_site_administration_tool

   select "Security"
   a page allows to select which part you want to manage (users, roles, allow / deny rules) (Ed: top truncated)

web_site_administration_user_roles

   select "Create user"
   an entry web page is displayed:

web_site_administration_create_user

   fill in the edit boxes and click "create user"


Please note:
  • all the Asp.Net administration pages are in source code in the following directory:

    web_site_administration_source_code

  • whenever we start the admin tool, there is some delay, since it has to find our security database and recompile the complete tool


3.3.3 - Direct Database User Entry with the Data Explorer

We can enter it using the Data Explorer:
   select "Data Explorer | dbExpress | BLACKFISHSQL | bfs_asp_admin | Tables | ASPNET_APPLICATIONS | right click | Retrieve Data From Table"

   the ASPNET_APPLICATIONS empty Table is displayed

   enter "my_app my_app 1001"

add_aspnet_users

and select "right click | Update"



Please note
  • the "Retrieve Data From Table" menu caption which allows Table inserts is quite strange. In fact, in our Dbx4 or Blackfish tutorials, we never succeeded, always trying to insert after the Table had been displayed using a double click.


3.3.4 - Direct Database User Entry with Delphi code

Rather than using either the Asp.Net configuration tool or the Database Explorer, we prefer to use Delphi code. This turns out to be a simple Asp.Net database exercise, but one never has enough of those.

First we will add a row to the ASPNET_USERS Table. All the rows are NOT NULL, so we must fill them all. To do this, we will use a parametrized query:

 
INSERT INTO aspnet_users
  (applicationiduseridusernameloweredusername
      isanonymouslastactivitydata)
  VALUES (1001, ?, ?, ?, False, ? )

And here is the code:
   reopen the previously created application "membership_admin" which created the ASPNET_USERS Table
   drop a new DataStoreCommand and, in the Object Inspector
  • rename it "insert_user_command_"
  • connect Connection to DataStoreConnection1
  • select "Parameters | ellipsis" and in the Parameter editor, add four parameters, three of type VarChar, and the last of type TimeStamp
   add two Textboxes for the user id and user name, and a Button, and in the Click event, add a new user to the Table:

const k_add_user_request=
           'INSERT INTO aspnet_users'
         + '  (applicationid, userid, username, loweredusername,'
         + '      isanonymous, lastactivitydata)'
         + '  VALUES (1001, ?, ?, ?, False, ? )'
         ;

procedure TDefault.add_user__Click(senderSystem.Object
    eSystem.EventArgs);
  begin
    DataStoreConnection1.Open;

    insert_user_comand_.CommandText:= k_add_user_request;
    insert_user_comand_.Prepare;

    insert_user_comand_.Parameters[0].Value:= id_textbox_.Text;
    insert_user_comand_.Parameters[1].Value:= user_name_textbox_.Text;
    insert_user_comand_.Parameters[2].Value:= user_name_textbox_.Text;
    insert_user_comand_.Parameters[3].Value:= DateTime.Now;

    insert_user_comand_.ExecuteNonQuery;

    DataStoreConnection1.Close;
  end// add_user__Click


   run and enter some users


To be able to start anew, we also added buttons for

  • dropping the Table
  • deleting all rows
  • displaying the users in a TextBox
Here is a snapshot of the application, after listing our users:

enter_users_by_code



Please note

  • we froze the application code to the 1001 value, but, of course, you could also use a Textbox to enter this value
  • we could not enter the dates in the Data Explorer, but since we do it by code this is of little importance
  • we chose to create the parameters in the Object Inspector, but they could also have been created by code (this is done in some of our later examples)


So far we entered an Application, a couple of users, but we also must fill the ASPNET_MEMBERSHIP Table which mainly contains the passwords.

We used the same technique, and added to this application the possibility to create this membership table, and enter some value.

Here is the creation request:

 
CREATE TABLE aspnet_membership
  ( 
     applicationid CHAR(36) NOT NULL,
     userid CHAR(36) NOT NULL PRIMARY KEY,
     password VARCHAR(128) NOT NULL,
     passwordformat INTEGER DEFAULT 0 NOT NULL,
     passwordsalt VARCHAR(128) NOT NULL,
     mobilepin VARCHAR(16),
     email VARCHAR(256),
     loweredemail VARCHAR(256),
     passwordquestion VARCHAR(256),
     passwordanswer VARCHAR(128),
     isapproved boolean NOT NULL,
     islockedout boolean NOT NULL,
     createdate TIMESTAMP NOT NULL,
     lastlogindate TIMESTAMP NOT NULL,
     lastpasswordchangeddate TIMESTAMP NOT NULL,
     lastlockoutdate TIMESTAMP NOT NULL,
     failedpasswordattemptcount INTEGER NOT NULL,
     failedpasswordattemptwindowstart TIMESTAMP NOT NULL,
     failedpasswordanswerattemptcount INTEGER NOT NULL,
     failedpasswordanswerattemptwindowstart TIMESTAMP NOT NULL,
     comment VARCHAR(10)
   )

and the insertion request (we enter only the mandatory fields):

 
INSERT INTO aspnet_membership
  (applicationiduserid,
     passwordpasswordformatpasswordsalt
     , isapprovedislockedout
     , createdatelastlogindatelastpasswordchangeddate
     , lastlockoutdate
     , failedpasswordattemptcountfailedpasswordattemptwindowstart
     , failedpasswordanswerattemptcount
     , failedpasswordanswerattemptwindowstart
    )
  VALUES 
    (
       1001, ?, 
       ?, ?, ? 
       , TrueFalse
       , ?, ?, ?, ? 
       , 0, ?
       , 0, ?
    );



In fact, the application .ZIP source code that you can dowload from our web site, contains code for creating, dropping, adding, deleting and listing the three Tables involved in membership management (application, users, membership).



3.3.5 - Managing the users

The Asp.Net application presented above, allowed us to directly enter the user and his password. In fact, behind the scenes, both the user and membership Tables are updated in sync.

We wrote an application which does the same, using the Tables already created. Here is a snapshot:

enter_user

The code simply uses the separate inserts of the previous application, and makes sure that the entered values are synchronized (same applicationid and same userid for each entry).

To use this application
   enter an application id and name
   enter the user id, name and password
The "generate ids" is there to quickly enter new users: we click on this button to generate a new value (say 7) and this will initialize the user id, name and password (200_7, user_7, pass_7) and increment the seed id.

Naturally you can enter more meaningful values instead of our artificial "user_4" etc.



3.4 - Membership Providers

3.4.1 - Provider definition

The Membership facilities work based on "Membership Providers". Providers specify a contract to manage some information:
  • the provider user can make some calls, usually based on some predefined abstract CLASSes
  • the provider implementor writes the code which stores and transfer the information, by implementing a descendent of the base provider CLASS and saving the data on disc.


3.4.2 - The Membership Provider System

In the case of Membership:
  • the basic contract is defined by the ABSTRACT MembershipProvider CLASS
  • Microsoft provided an SqlMembershipProvider implementation, and CodeGear wrote a BlackfishMembershipProvider and an InterbaseMembershipProvider
  • for specific behaviour, it is of course possible to write other MembershipProviders: some will save the data as .XML file, some will use ODBC as data access, or even .TXT files
Each of those providers manages the security data (user names, password, e-mail etc) to its own persistent media: Sql Server for the SqlMembershipProvider, BlackfishSql for the BlackfishMembershipProvider etc.

At the developper level, we use a Membership CLASS which only calls the methods defined in the ABSTRACT CLASS. The consequence is that if we create any other descendent of the MembershipProvider, our Asp.Net code should not be modified in any way. We decoupled the Membership calls of our applications from the way the data is stored on disc.



This can be represented with the following diagram:

membership_class



A couple of notes

  • the database schema used to persist the security data on disc is irrelevant. The developer code should only call the Membership methods, and the providers will take care of talking to the database.
  • in fact we could use different Tables that those presented above, if we are willing to write a MembershipProviders which talks to those Tables
  • therefore directly reading and writing data to the ASPNET_APPLICATIONS, ASPNET_USERS and ASPNET_MEMBERSHIP Tables is not a good idea. The schema we presented is the one implemented by Delphi, and it closely follows the Microsoft schema. But those could change in the future, whereas the ABSTRACT MembershipProvider CLASS should remain more stable.
    For instance using the Asp.Net Configuration application should be safe. We directly wrote to the Tables because we felt it was quicker (= could be replayed very easy).


3.4.3 - The Web.Config Membership entries

To specify which MembershipProvider should be used, we must add some entries to Web.Config
  • a <membership section
  • if we use some database, a <connectionString section
and, of course, the <authentication and <authorization sections



Here is the default Web.Config (Ed : truncated):

 
<configuration>
  <connectionStrings>
    <remove name="BlackfishSqlAspNet"/>
    <add name="BlackfishSqlAspNet"
        connectionString="database=|DataDirectory|bsql_aspnetdb.jds;" ...ooo...
        providerName="Borland.Data.BlackfishSQL.RemoteClient" />
  </connectionStrings>
 
  <system.web>
    <authorization>
      <deny users="?" />
    </authorization>
 
    <authentication mode="Forms">
      <forms name="my_cookie" loginUrl="my_login.aspx">
      </forms>
    </authentication>
 
    <membership defaultProvider="AspNetAdoMembershipProvider">
      <providers>
        <remove name="AspNetAdoMembershipProvider" />
        <add
            name="AspNetAdoMembershipProvider"
            type="Borland.Web.Security.AdoMembershipProvider,
                Borland.Web.Provider, Version=11.0.5000.0,
                Culture=neutral, PublicKeyToken=91d62ebb5b0d1b1b
"
            connectionStringName="BlackfishSQLAspNet"
            applicationName="/"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="50"
            minRequiredPasswordLength="8"
            minRequiredNonalphanumericCharacters="1"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression="" />
      </providers>
    </membership>
 
  </system.web>
</configuration>

Here is another example with our own MembershipProvider:

 
<configuration xmlns="http://schemas" ...ooo...>
  <connectionStrings>
    <add
        name="my_connection"
        connectionString="host=LocalHost;
            database=C:\programs\us\web\asp_net\
            membership_roles_profile\_data\
            bfs_membership_manual.jds;
            user=sysdba;password=masterkey
"
        providerName="Borland.Data.BlackfishSQL.RemoteClient"/>
  </connectionStrings>
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>
 
    <authentication mode="Forms"/>
 
    <membership defaultProvider="my_membership">
      <providers>
        <remove name="my_membership"/>
        <add
            name="my_membership"
            type="u_c_membership_provider.c_membership"
            requiresQuestionAndAnswer="true"
            connectionStringName="my_connection"
            applicationName="/"/>
      </providers>
    </membership>
  </system.web>
</configuration>



And:

  • the <connectionString section specify database connections. In our case, there is only the MembershipProvider database
  • the <membership section contains
    • a provider name (any name will do)
    • the MembershipProvider CLASS
    • eventually, the database to use
As you see, our MembershipProvider database is located outside the Asp.Net application path (not in the APP_DATA or each application), which allows us to avoid reinitializing the whole thing for every of our samples. In addition, the .ZIP will contain the corresponding MembershipProviders .DLLs (but you can easily revert to the default Delphi MembershipProviders)



3.5 - Using the Membership CLASS

3.5.1 - The Membership CLASS

Once the MembershipProvider is in place, we can
  • either directly call Membership methods
  • or use predefined Microsoft login components which indirectly use the Membership CLASS


3.5.2 - The MembershipUser CLASS

We can retrieve information about the current logged in User, or find a user, even retrieve all Users using the Membership CLASS.

Many of the querying functions return a MembershipUser instance, or a MembershipUserCollection.

This can be represented like this (Ed: partial):

membership_user

The MembershipUser CLASS allows us to perform many usual User management tasks:

  • change the password
  • find a User using his email (when he forgot his password)
But this CLASS will not return the password, which is hidden inside the persistent data, and only used by to MembershipProvider to return the ValidateUser result.



3.5.3 - Using the Membership CLASS in code

Here is an example which displays all the users
   select the Default.Aspx page, drop a TextBox and a Button which displays all the users in the TextBox:

procedure TDefault.list_users__Click(senderSystem.Object;
    eSystem.EventArgs);
  var l_c_membership_user_collectionMemberShipUserCollection;
      l_i_user_enumeratoriEnumerator;
  begin
    l_c_membership_user_collection:= MemberShip.GetAllUsers();

    l_i_user_enumerator:= l_c_membership_user_collection.GetEnumerator;
    while l_i_user_enumerator.MoveNext do
      with l_i_user_enumerator.Current as MembershipUser do
        display(UserName' 'PasswordQuestion);
  end// list_users__Click




Note that we could have bound the MemberShipUserCollection directly to some Grid component.



3.6 - The Asp.Net 20 Login Controls

The login components cover most of the tasks that we had to manually create for user management:
  • the most obvious ones:
    • Login which simply let the User enter his credentials (exactly like we did in our first example)
    • LoginView which is a templated (parametrized) version of the same
    • LoginStatus which allows to logout, or, for anonymous users, to login
    • PasswordRecovery which does what the name says
    • ChangePassword allows a User to change his password
    • LoginName is used to display the name of the User: "Hello SuchAndSuch, we are very happy to have you onboard, winds are East-North East, the ground temperature is 55" etc
  • CreateUserWizard is a multi page component for creating new users


3.7 - The Login Component

The most obvious component is the Login component. Two TextBoxes, a Button and your done.

Well, not quite. This is a deluxe version of the usual login. You naturally have labels which specify what the TextBoxes should contain, but also a validator, and many configurable features.



Here is a simple example of using this control:
   select "File | New | Asp.Net Web Application", specify the path and app name, like "membership_login"
   run the project once, and enter some users (or copy the BSQL_ASPNETDB.JDS files from a previously initialized database to the local APP_DATA folder)
   select "File | New | Delphi for .Net | New Asp.Net file | Asp.Net Page" and name this page LOGIN.ASPX
   select "Tool Palette | Web Login"
   all the security components are presented:

web_login

   select Login and drop it on the Form
   select the DEFAULT.ASPX page, and drop a Button, just to be sure this page does not appear just blank
   open the WEB.CONFIG file, and make sure that
  • the <connectionString section contains your database connection string (the default should be allright)
  • the <membership section contains a reference to this database
  • the <authentication specify WindowForms
  • the <authorization sections denies anonymous users
Check the figure above to be sure this is all setup
   compile and run
   the login page is presented:

web_login_component

   enter one of your users credentials. In our case "user_1" and "pass_1" will be fine
   the Default page is presented


3.7.1 - Customizing the Login

We can customize the Login in many ways
  • first there are the usual Object Inspector properties. Background color, label captions, default user etc can be all adjusted. Here is an example:

    web_login_customization

    As usual, those properties can be directly setup in the .ASPX file

  • we can also use .CSS style sheets for modifying the Login style

  • we can also add validation rules or add additional controls using TEMPLATEs


Here is how to check non-empty user name input and add an e-mail input:

 
<%@ Page language="c#" Debug="true" Codebehind="Login3.pas"
    AutoEventWireup="false" Inherits="Login3.TLogin3" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" >
<html>
  <body>
    <form runat="server">
      <asp:Login id="Login1" runat="server" width="80px"
          backcolor="#C0C0FF">
        <LayoutTemplate>
          <table cellspacing="1" cellpadding="1"
              width="300" border="1">
            <tr>
              <td>
                User name
              </td>
              <td>
                <asp:textbox id="UserName" runat="server"/>
              </td>
              <asp:RequiredFieldValidator
                  id="RequiredFieldValidator1"
                  runat="server"
                  ControlToValidate="UserName"
                  errormessage="Username cannot be blank"/>
            </tr>
            <tr>
              <td>
                Password
              </td>
              <td>
                <asp:textbox id="Password" runat="server"/>
              </td>
            </tr>
            <tr>
              <td>
                e-mail
              </td>
              <td>
                <asp:textbox id="Email" runat="server"/>
              </td>
            </tr>
          </table>
        </LayoutTemplate>
        <asp:Button id="Login" runat="server"
          text="Login" CommandName="Login"/>
      </asp:Login>
    </form>
  </body>
</html>

which will look like this:

templated_login_with_validation

Sure, nothing fancy, but if the user does not enter his name, he will receive an accurate error message. We could of course add more sophisticated validation, using regular expression validators (only letters etc)



3.7.2 - Login Control Properties and Events

The Login control has several properties and events (Ed: partial)

login_control_properties_and_events

The events fire in the following order:

login_control_event_order



Here is an example which redirects the user to the URL specified in the Login.PasswordRecoveryUrl if the error count exceeds 5:

procedure TLogin4.Page_Load(senderSystem.Object;
    eSystem.EventArgs);
  begin
    // -- initialize the error count
    if Not IsPostBack
      then ViewState['my_error_count']:= '0';
  end// Page_Load

procedure TLogin4.Login1_LoginError(senderSystem.Object;
    eSystem.EventArgs);
  var l_error_countInteger;
  begin
    // -- increase the error count
    l_error_count:= Convert.ToInt32(ViewState['my_error_count'])+ 1;
    ViewState['my_error_count']:= l_error_count.ToString;

    // -- redirect if more than 5
    if l_error_count> 5
      then Response.Redirect(Login1.PasswordRecoveryURL);
  end// Login1_LoginError



3.8 - The LoginStatus Control

This control simply displays
  • either a "Logout" hyperlink
  • or a "Login" hyperlink
When the user is authenticated, the page containing this control displays "logout". Clicking the hyperlink will logout, and if we have initialized the LoginStatus.LogoutPageUrl property the user is then redirected to this page.

When the user is not authenticated, the page will display "login", and clicking this link will redirect the user to the default login page.



3.8.1 - The ChangePassword, LoginName, PasswordRecovery and LoginView Controls

Those controls behave exactly as anybody would expect them to behave.

Just a couple of comments:

  • the PasswordRecovery uses SMTP to sent the lost password to the User and this is configured once again using Web.Config
  • the LoginView is a TEMPLATEd control allows to display different elements based on the status of the user (logged in or not) or even based on the user role. So it uses the TEMPLATE technique, but what is displayed depends on the User status


3.8.2 - The CreateUserWizard Control

Finally we are going to examine the CreateUserWizard which is a new feature of Asp.Net 2.0

Wizard controls are a kind of elaborate MultiView control. They contain steps corresponding to different pages, and the navigation is automatically built-in. We configure those controls using the "Smart Tabs", which are some kind of menus (actually windows) which are opened and closed using the right-top arrow of the control.

For the CreateUserWizard, there are two mandatory "steps"

  • one step (page) for entering the User parameters (name, password, e-mail) etc
  • another step which reports any error (duplicate user name etc) or success, and lets the User navigate to some other page


Here are the steps:
   select "File | New | Delphi for .Net | New Asp.Net file | Asp.Net Page" and name this page CREATE_USER_WIZARD.ASPX
   select "Tool Palette | Web Login | CreateUserWizard" and drag it on the Form:
   the control is displayed, with the first step and the smart tab menu open:

create_user_wizard

   change any properties you like in the Object Inspector
   open the "smart tab" menu by right clicking the top right small arrow. Then select "Step | Complete"
   the second step is displayed:

create_user_wizard_complete

   in the Object Inspector, specify the ContinueDestinationationUrl. We simply chose ~/Default.aspx
   in the smart tab, select the first step again

   select the Default page, add a Button which will transfer the control to the create_user_wizard.aspx page:

procedure TDefault.create_user__Click(senderSystem.Object
    eSystem.EventArgs);
  begin
    Server.Transfer('create_user_wizard.aspx'True);
  end// create_user__Click

   compile, run, enter your credentials, in the Default page, click "create_user_"
   the first step of the wizard is displayed

enter_credentials

   fill the credential Textboxes and click "Create User"
   if the operation succeeds you will see:

enter_credentials_success

   click "Continue"
   the Default page is presented


Of course there are numerous configuration possibilities of the content of the steps, and of the number of steps.




4 - Authorization and Roles

4.1 - Using Roles

When our site only accepts a couple of Users, we can specify the credentials in Web.Config. For more important sites, the use of some security database is obvious.

In addition to storing user names and passwords (authentication), we usually also specify which pages each User is allowed to request (authorization). Here also, Web.Config can be used with individual User references, but for many Users, we can group them in Roles. For instance "Admin", "Guest", "Accounting" etc. Those roles can the be used anywhere the user name were used for authorization purposes.



4.2 - Role service architecture

The Roles services are structured exactly like the Membership services, using:
  • a RoleProvider which is an ABSTRACT CLASS defining the available methods
  • a Roles CLASS which we can use to manage the roles
Here is the UML Class Diagram of those CLASSes (Ed: partial):

role_provider



Please note

  • those CLASSes are MUCH simpler to implement, since they are mainly handling STRINGs or ARRAY OF STRINGs
  • this is probably the reason why there is no separate "role" CLASS (whereas there was a "user" CLASS)


4.3 - Creating the Roles Table

As explained above, the mainstream developer does not need to create the Roles table and to fill them by code: he should stay above the RolesProvider layer, and only use either the Asp.Net configuration tool, or the Roles CLASS.



Nevertheless, we included a .ZIP source file which creates the Tables and drops them, and a second application which fills some value by directly writing into the Role Tables.



There are two Tables involved: ASPNET_ROLES and ASPNET_USERSINROLES. The general schema has been presented
above.



To create the ASPNET_ROLES Table, we can use the following request:

 
CREATE TABLE aspnet_roles
  (
    applicationid CHAR(36) NOT NULL,
    roleid CHAR(36) NOT N