Using a Custom Membership Provider in an MVC Application

I prefer to have a full control of my database objects – which is one reason why I do not want to use the data structures available by default in the ASP.Net Membership controller. In addition because the site uses WCF to provide a publicly available library I do not want to constantly transmit someone’s actual login credentials.

What I am going to do is assign a one-time key each time someone successfully logins in. This key will then allow access to the library functionality until either the session times out or the user logs out.

The application will be multi-tier.

1. SQL Server database

2. Entity Model

3. WCF Layer

4. Client Layer

For the purposes of the prototype we have an MVC Client which provides two other layers

4a. Model Layer

4b Controller

4c View.

Layers 2 – 4 are implemented by four projects within the Visual Studio Solution.

2. Entity Model (MyApp.Data.Model)

3. WCF Layer (MyApp.Data.DataAccess & MyApp.Data.Services)

4. MVC Client (MyApp.Portal)

The Database layer

Membership Database Model

Membership Database Model

Above is the Entity Model which illustrates our database that sits behind the membership provider.  The RegisteredUser contains details of the user and has the following fields;

Id Primary Key
RequestHostAddress IP Address of the workstation used when the account was first registered
RequestDate The date of Registration
Name The name of the user
EmailAddress The email address of the user – this acts as a login as well as providing a means of communicating with a user
Password The password associated with the user – this will be stored in an encrypted form

When you use the entity modeler it provides a means of generating a script to create your database. Once the diagram has been created right click on the model

The Entity Model Context Menu

Select the Generate Database from Model option. This will then show ‘Summary and Settings’ window. Click Finish and then a script to create your database will be added to your project.

Below is the code that creates my database

-- --------------------------------------------------
-- Entity Designer DDL Script for SQL Server 2005, 2008, and Azure
-- --------------------------------------------------
-- Date Created: 06/19/2010 17:39:48
-- Generated from EDMX file: C:\Users\Paul\documents\visual studio 2010\Projects\Simplicita.Portal\Simplicita.Data.Model\SimplicitaModel.edmx
-- --------------------------------------------------
SET QUOTED_IDENTIFIER OFF;
GO
USE [Simplicita];
GO
IF SCHEMA_ID(N'dbo') IS NULL EXECUTE(N'CREATE SCHEMA [dbo]');
GO
-- --------------------------------------------------
-- Dropping existing FOREIGN KEY constraints
-- --------------------------------------------------
-- --------------------------------------------------
-- Dropping existing tables
-- --------------------------------------------------
IF OBJECT_ID(N'[Users].[LoginLog]', 'U') IS NOT NULL
DROP TABLE [Users].[LoginLog];
GO
IF OBJECT_ID(N'[Users].[RegisteredUsers]', 'U') IS NOT NULL
DROP TABLE [Users].[RegisteredUsers];
GO
-- --------------------------------------------------
-- Creating all tables
-- --------------------------------------------------
-- Creating table 'LoginLogs'
CREATE TABLE [dbo].[LoginLogs] (
[SessionId] varchar(60)  NOT NULL,
[UserId] bigint  NOT NULL,
[LoginDate] datetime  NOT NULL,
[HostAddress] varchar(30)  NOT NULL
);
GO
-- Creating table 'RegisteredUsers'
CREATE TABLE [dbo].[RegisteredUsers] (
[Id] bigint IDENTITY(1,1) NOT NULL,
[RequestHostAddress] varchar(20)  NOT NULL,
[RequestDate] datetime  NOT NULL,
[Name] varchar(60)  NOT NULL,
[EmailAddress] varchar(max)  NOT NULL,
[Password] varchar(60)  NOT NULL
);
GO
-- --------------------------------------------------
-- Creating all PRIMARY KEY constraints
-- --------------------------------------------------
-- Creating primary key on [SessionId], [LoginDate] in table 'LoginLogs'
ALTER TABLE [dbo].[LoginLogs]
ADD CONSTRAINT [PK_LoginLogs]
PRIMARY KEY CLUSTERED ([SessionId], [LoginDate] ASC);
GO
-- Creating primary key on [Id] in table 'RegisteredUsers'
ALTER TABLE [dbo].[RegisteredUsers]
ADD CONSTRAINT [PK_RegisteredUsers]
PRIMARY KEY CLUSTERED ([Id] ASC);
GO
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
-- --------------------------------------------------
-- Script has ended
-- --------------------------------------------------
-- ---------------------------------------------------- Entity Designer DDL Script for SQL Server 2005, 2008, and Azure-- ---------------------------------------------------- Date Created: 06/19/2010 17:39:48-- Generated from EDMX file: C:\Users\Paul\documents\visual studio 2010\Projects\Simplicita.Portal\Simplicita.Data.Model\SimplicitaModel.edmx-- --------------------------------------------------
SET QUOTED_IDENTIFIER OFF;GOUSE [Simplicita];GOIF SCHEMA_ID(N'dbo') IS NULL EXECUTE(N'CREATE SCHEMA [dbo]');GO
-- ---------------------------------------------------- Dropping existing FOREIGN KEY constraints-- --------------------------------------------------

-- ---------------------------------------------------- Dropping existing tables-- --------------------------------------------------
IF OBJECT_ID(N'[Users].[LoginLog]', 'U') IS NOT NULL    DROP TABLE [Users].[LoginLog];GOIF OBJECT_ID(N'[Users].[RegisteredUsers]', 'U') IS NOT NULL    DROP TABLE [Users].[RegisteredUsers];GO
-- ---------------------------------------------------- Creating all tables-- --------------------------------------------------
-- Creating table 'LoginLogs'CREATE TABLE [dbo].[LoginLogs] (    [SessionId] varchar(60)  NOT NULL,    [UserId] bigint  NOT NULL,    [LoginDate] datetime  NOT NULL,    [HostAddress] varchar(30)  NOT NULL);GO
-- Creating table 'RegisteredUsers'CREATE TABLE [dbo].[RegisteredUsers] (    [Id] bigint IDENTITY(1,1) NOT NULL,    [RequestHostAddress] varchar(20)  NOT NULL,    [RequestDate] datetime  NOT NULL,    [Name] varchar(60)  NOT NULL,    [EmailAddress] varchar(max)  NOT NULL,    [Password] varchar(60)  NOT NULL);GO
-- ---------------------------------------------------- Creating all PRIMARY KEY constraints-- --------------------------------------------------
-- Creating primary key on [SessionId], [LoginDate] in table 'LoginLogs'ALTER TABLE [dbo].[LoginLogs]ADD CONSTRAINT [PK_LoginLogs]    PRIMARY KEY CLUSTERED ([SessionId], [LoginDate] ASC);GO
-- Creating primary key on [Id] in table 'RegisteredUsers'ALTER TABLE [dbo].[RegisteredUsers]ADD CONSTRAINT [PK_RegisteredUsers]    PRIMARY KEY CLUSTERED ([Id] ASC);GO
-- ---------------------------------------------------- Creating all FOREIGN KEY constraints-- --------------------------------------------------
-- ---------------------------------------------------- Script has ended-- --------------------------------------------------

Stored Procedures

All my database manipulation occurs behind stored procedures; the script above which is created by the data modeler concerns itself only with objects – it does not create the stored procedures; For the Membership Provider we have two procedures; one for Registering the user, and one for validating their login when they logon.

The Registration Stored Procedure is as follows;

/****** Object:  StoredProcedure [Users].[RegisterUser]    Script Date: 06/19/2010 18:17:52 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER Procedure [Users].[RegisterUser]
	@RequestHostAddress	VarChar(20),
	@Name VarChar(60),
	@EmailAddress VarChar(MAX),
	@Password VarChar(60)
As
	Declare @LastRec BigInt

	Insert Into Users.RegisteredUsers
                (RequestHostAddress, RequestDate, Name, EmailAddress, Password)
	Values		(@RequestHostAddress, GETDATE(),@Name,@EmailAddress,@Password)

	Set @LastRec = SCOPE_IDENTITY()

	Select		Id,
			RequestHostAddress,
			RequestDate,
			Name,
			EmailAddress,
			Password
	From            Users.RegisteredUsers
	Where		Id = @LastRec

It is fairly simple – the details provided by the user, plus their current IP Address and the date are inserted into the database. The record added is then returned to the calling application. This is in part due to a weakness in the Entity Model used with .Net 3.5 that only seems to execute stored procedures if an entity is returned. I’m not sure why this was the case, but the functionality is much improved under .Net 4 which not only allows stored procedures which return nothing, but also complex types which are not the same as existing database.

The Login is a little more complex

/****** Object:  StoredProcedure [Users].[Login]    Script Date: 06/19/2010 18:24:01 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER Procedure [Users].[Login]
	@Email	VarChar(MAX),
	@Password VarChar(60),
	@SessionId VarChar(60),
	@HostAddress VarChar(30)
As
Begin

	Declare @UserId BigInt
	Declare @LogId BigInt

	Set NoCount On;

	If Exists(Select * From Users.RegisteredUsers As RU Where (EmailAddress = @Email) AND (Password = @Password))
	Begin
		Set @UserId = (Select Id From Users.RegisteredUsers Where (EmailAddress = @Email) And (Password = @Password))

		INSERT INTO Users.LoginLog
                    (SessionId, UserId, LoginDate, HostAddress)
		VALUES      (@SessionId,@UserId, GETDATE(),@HostAddress)	

	End 

	Select	SessionId,
			UserId,
			LoginDate,
			HostAddress
	From Users.LoginLog
	Where SessionId = @SessionId
END

Basically we first look for a record that matches the logon criteria given by the user – note: passwords are stored encrypted, this procedure expects an encrypted password and checks this against the database. This is done in part to prevent someone from knowing a users password if they gain access to the database. Also if someone does sniff the line to get the login details they are only transmitted once and not unencrypted.

If the record exists then an entry is inserted into LoginLog – this will be used later in the project when accessing a users data. Basically when they SessionId is passed this is checked against this table and hence to link to the users data. When we want to signoff this entry is flagged so it will not be picked up again.

This model has the added advantage that we now have a record of when a user logs in, where they logged in from and when.

Setting up the Data Model to use the Stored Procedures

Select the Data Model in your project and view the Model Browser.

Taking each Stored Procedure in turn

1. Right click and select ‘Add Function Import’

2.

Add Function Import

Add Function Import

Select the Entities option and pick the relevent entity from the pulldown.

3. Click on the Get Column Information and then  click OK.

That now sets up our Stored Procedures up and completes the database setup. Now it is time to move to the WCF Layer

The WCF Layer

There are a couple of references we need to set up. Right click on Web Service Project and select Add Reference

Add Reference

Add Reference

From the .Net Libraries select the System.Data.Entities library – this is needed by projects which themselves access a separate library with an Entity Model in it. From the Projects Library select the MyApp.Data.AccessObjects and the MyApp.Data.Model projects.

That is the references set-up.

Now we need to copy the database connection string from the Entity project to the Web Service. This can be found in the App.Config

<add name="MyEntities" connectionString="metadata=res://*/MyModel.csdl|res://*/MyModel.ssdl|res://*/MyModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=MYSERVER\SQLEXPRESS;Initial Catalog=Simplicita;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />

This needs to be copied to the Web.Config of the Web Service.

Well that is the setting up. Now we need to create the objects that will be sent back from the Webservice. These need to be setup so that they will be serialised when sent over the internet.

I’ve created two objects for the Membership Provider (so far – I reserve the right to create loads more)

User.cs – The User Data Access Object

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;

namespace MyApp.Data.AccessObjects.UserAccount
{
    [DataContract]
    public class User
    {
        [DataMember]
        public String Name { get; set; }

        [DataMember]
        public String Email { get; set; }

        [DataMember]
        public String Password { get; set; }

        [DataMember]
        public String CurrentSessionId { get; set; }

        [DataMember]
        public String HostAddress { get; set; }

    }
}

We use the DataContract and the DataMember Attributes so that the WCF class can serialize the object. These attributes are defined in the System.Runtime.Serialization namespace and there is a using command for this at the top of the class.

Really nifty tip

If you are anything like me then you can never remember what namespace any given object or class is in. However in VS 2010 there is a really nifty little facility which tells you what namespace, and even inserts it into the code for you!. Right click on the class or object. Select ‘Resolve’ from the context menu and then  select the required namespace.

Why create seperate objects?

Now of course the Data Entity Library creates it’s own classes, but these are tied very closely to the structure of the database. This is something we do not want, since changes in the database will be reflected within other layers. By creating these data access objects we also create an extra layer of abstraction between the database and the middle tier. We create them in a seperate project so that they can be used (if ever required) without having to create a reference to the Web Service.

LoginEntry.cs – the Login results

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;

namespace Simplicita.Data.AccessObjects.UserAccount
{
    [DataContract]
    public class LoginEntry
    {
        [DataMember]
        public String SessionId { get; set; }

        [DataMember]
        public long UserId { get; set; }

        [DataMember]
        public DateTime LoginDate { get; set; }

        [DataMember]
        public String HostAddress { get; set; }
    }
}

The Login entry returns the results of the login. This will be a duplicate the entry placed in the Login Log created in the database.

Finally to the WebService

OK, so the objects have been created, references set up – now is the time to write the service which will log our user on. Because the interface will be available over the wen I am seperating each service into smaller chunks. So for the Membership Provider we have a UserAccount Service.

A webservice exposes it’s messages via an interface. So the first thing to do is implement the interface. For this article I am concentrating on the actual login process. So this interface would need to implement other parts such as the registration, password reminder and all that. So this only shows the Login Process or ValidateAccount;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using MyApp.Data.AccessObjects.UserAccount;

namespace MyApp.Data.Services
{
    [ServiceContract]
    public interface IUserAccount
    {
        [OperationContract]
        String ValidateAccount(String UserName, String Password); 

    }
}
The interface is given the ServiceContract attribute since this is the contract between the service and the client. Each Operation then has it’s own Contract. The attribute is available from the System.ServiceModel attribute. We also refer to the Data.AccessObjects.UserAccount namespace which contains the Data Access Objects. The UserAccount namespace is created because I created a folder called UserAccount in the project. Visual Studio then adds the Folder name to the namespace – it is a good way of organising a project, not least because it separates objects and allows us to use the same object name more than once.
The Service itself is where it gets interesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using Simplicita.Data.AccessObjects.UserAccount;
using Simplicita.Data.Model;
using System.ServiceModel.Channels;
using System.Security.Cryptography;

namespace Simplicita.Data.Services
{
    public class UserAccount : IUserAccount
    {
        private const string ENCRYPTION_KEY = "MyKey";

        private String GetIPAddress()
        {
            OperationContext context = OperationContext.Current;
            MessageProperties prop = context.IncomingMessageProperties;
            RemoteEndpointMessageProperty endpoint = prop[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
            string ip = endpoint.Address;

            return ip;

        }

        private string EncryptString(string Message, string Passphrase)
        {
            byte[] Results;
            System.Text.UTF8Encoding UTF8 = new System.Text.UTF8Encoding();

            // Step 1. We hash the passphrase using MD5
            // We use the MD5 hash generator as the result is a 128 bit byte array
            // which is a valid length for the TripleDES encoder we use below

            MD5CryptoServiceProvider HashProvider = new MD5CryptoServiceProvider();
            byte[] TDESKey = HashProvider.ComputeHash(UTF8.GetBytes(Passphrase));

            // Step 2. Create a new TripleDESCryptoServiceProvider object
            TripleDESCryptoServiceProvider TDESAlgorithm = new TripleDESCryptoServiceProvider();

            // Step 3. Setup the encoder
            TDESAlgorithm.Key = TDESKey;
            TDESAlgorithm.Mode = CipherMode.ECB;
            TDESAlgorithm.Padding = PaddingMode.PKCS7;

            // Step 4. Convert the input string to a byte[]
            byte[] DataToEncrypt = UTF8.GetBytes(Message);

            // Step 5. Attempt to encrypt the string
            try
            {
                ICryptoTransform Encryptor = TDESAlgorithm.CreateEncryptor();
                Results = Encryptor.TransformFinalBlock(DataToEncrypt, 0, DataToEncrypt.Length);
            }
            finally
            {
                // Clear the TripleDes and Hashprovider services of any sensitive information
                TDESAlgorithm.Clear();
                HashProvider.Clear();
            }

            // Step 6. Return the encrypted string as a base64 encoded string
            return Convert.ToBase64String(Results);
        }

        public String ValidateAccount(string UserName, string Password)
        {
            MyAppEntities       ctx                 = null;
            LoginEntry          LoginResult         = null;
            Guid                SessionId           = Guid.NewGuid();
            String              sSessionId          = String.Empty;
            String              encryptedPassword   = String.Empty;
            String              result              = String.Empty;

            try
            {
                sSessionId = SessionId.ToString();
                encryptedPassword = EncryptString(Password, ENCRYPTION_KEY);

                ctx = new SimplicitaEntities();

                LoginResult = (LoginEntry)((from r in ctx.Login(UserName, encryptedPassword, sSessionId, GetIPAddress())
                                       select new LoginEntry {
                                           SessionId = r.SessionId,
                                           UserId = r.UserId,
                                           LoginDate = r.LoginDate,
                                           HostAddress = r.HostAddress
                                       }).First());

                if (LoginResult != null)
                {
                    result = LoginResult.SessionId;
                }

            }
            catch (System.Exception ex)
            {
                result = String.Empty;
            }

            return result;
        }
    }
}

The first function (GetIPAddress) gets the IP address – this is part of the security setup. Basically it records the IP address that a login attempt comes from, and allows tracking logins. Next a function to encrypt the password. I’m not interested in de-encrypting the password  – if we always encrypt the password then they are not stored in the clear. I do question where the encryption should be. This design means the password is passed to the service in the clear. All very well. The reason it is not done by the calling client is because it leaves the encryption (except the ‘millions’ who read this – I can live in hope 😉 ) behind the service. As any developer will be able to use the service, if the encryption was in the client I would have to give the encryption process to any calling developer.

These are both private and do not need to be exposed to the wider internet – they are helper functions for the last function – ValidateAccount.

First off we set the sessionId – this is a GUID. We put it here, because we do not want anyone to spoof the site, by calling with a GUID. This GUID is returned by the function which the user will subsequently use in the header of messages to achieve the results they want, or get their data.

Next we encrypt the password, calling our helper function to do so.

The ctx holds an instance of our Database entity. We use this to call our Stored Procedure we created earlier. I’ll split this line up from the inside to the outside;

select new LoginEntry {
                           SessionId = r.SessionId,
                           UserId = r.UserId,
                           LoginDate = r.LoginDate,
                           HostAddress = r.HostAddress
                      }).First()

Here is a LINQ query which creates an instance of the LoginEntry class – this was the Data Access Object we created earlier. We want the First instance. The function returns only two possible number of records. Zero for an invalid login and 1 if it is valid.

(LoginEntry)((from r in ctx.Login(UserName, encryptedPassword, sSessionId, GetIPAddress())
                                       select new LoginEntry {
                                           SessionId = r.SessionId,
                                           UserId = r.UserId,
                                           LoginDate = r.LoginDate,
                                           HostAddress = r.HostAddress
                                       }).First());

Here we cast the result to an instance of the LoginEntry object. If there is no entry this causes an exception. When an exception is thrown the result is set to an empty string. If there is a row then the login was successful and the result is set to the SessionId and returned to the calling application.

The MVC Client.

So we now have a service, which makes a call to a database – validates the login criteria and returns a SessionId to identify the user in subsequent calls to the service. Now it comes time to create the MVC Client which will call this service.
I used the empty MVC Service – this meant the new client did not create a MembershipProvider. On the downside it also does not create a MasterPage. This is out of scope for this article, but creating the MasterPage is not that difficult.
MVC stands for Model, View Controller and in line with the rest of this entry I am moving from the back to the front – which means the first thing is the ‘Model’, but before then we need to set up a service reference to the WCF service. I covered how to do this in an earlier article here;

https://developingmycloudapp.wordpress.com/2010/06/19/connecting-a-wcf-service-to-an-model-view-controller-client/

The Model

To implement a Custom Membership Provider you inherit from the MembershipProvider. This has a number of abstract methods which implement all the functionality required to manage the Membership of the site. Once the class has been created right click on the class name and select the implement abstract class. This will create stub entries for every method that needs to be implemented. You don’t need to write all of them, just ensure the stubs are there. For this article I am only concentrating one one method – the ValidateUser one – and here we are going to see why layering an application and doing all this work provides dividends.
        public override bool ValidateUser(string username, string password)
        {
            UserAccountClient ctx = null;

            try
            {
                ctx = new UserAccountClient();
                SessionId = ctx.ValidateAccount(username, password);

            }
            catch
            {
                return false;
            }
            return !String.IsNullOrEmpty(this.SessionId);
        }

The main body of this is actually only one line – when we create a reference to a webservice it creates a proxy class which handles all the messy communication between the service and the client. We just call the method created in that proxy class. The actual body of this function is just one line. The rest is just initializing the various instances.

There is however one other function used here, which was not created when we implemented the required Abstract Class methods. The SessionId will hold the session id created by the call to the login service. This function is below;

public String SessionId { get; set; }

That’s it – a whole method in one line of C#. This takes advantage of a facility that was introduced into C# 3. Basically instead of all that tedious mucking about creating fields and then the property, C# sharp can do it for us. This is then available to the next stage up to get the SessionId

‘C’ is for Controller

Nearly at the home stretch now, and time to implement the controller.

    public class AccountController : Controller
    {

        MyMembershipProvider provider = (MyMembershipProvider)Membership.Provider;
        .
     }

Above is the stub of a controller. All Controllers inherit from the Controller class. We also have a class wide instance of our provider.

        public ActionResult Logon()
        {

            return View();
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Logon(string username, string password, string returnUrl)
        {
            if (!ValidateLogin(username, password))
            {

                return View();
            }

            FormsAuthentication.SetAuthCookie(provider.SessionId, false);

            if (String.IsNullOrEmpty(returnUrl))
                return Redirect(returnUrl);
            else
                // TODO: Set the redirect to the company list when we are ready
                return RedirectToAction("Index","Home");
        }

Each model has a verb and this implements one such verb – this is the logon verb and will be called when ever a user has to login to the application. It consists of two functions; one when the user goes to the login page, the other when they have given their login criteria and it needs validating.

When a user is validating this action verb makes a reference to the ValidateLogin function

        private bool ValidateLogin(string UserName, string Password)
        {
            if (String.IsNullOrEmpty(UserName))
            {
                ModelState.AddModelError("UserName","You must provide a User Name");
            }

            if (String.IsNullOrEmpty(Password))
            {
                ModelState.AddModelError("Password", "You must provide a Password");
            }

            if (!provider.ValidateUser(UserName, Password))
            {
                ModelState.AddModelError("_FORM", "Unknown username and/or password");
            }

            return ModelState.IsValid;
        }

Basically this validates the inputs and finally makes a call to the ValidateUser method of the provider we created earlier. Back to Post version of our Action Verb if the input is valid and we recognise the user then we use a call to the SetAuthCookie method of the FormsAuthentication static class – which sets the authentication cookie that can be used to later identify the user.

‘V’ is for View

Last 20o yrds now – and how the user is going to implement the interface that the user sees when they use the application. Right click on the first function (Login) and select AddView

Add View Window

Add View Window

We don’t need to create a strongly viewed type so we can just click ‘OK and this will create our client page.

This will create a web page – with two asp:Content controls; The first is the title content, the next is the body where we put the following code;

    <h2>Logon</h2>
    <% using (Html.BeginForm())
    { %>
    <table>
        <tr>
            <td><asp:Label ID="Label1" runat="server" Text="Username:" CssClass="FormLabel"></asp:Label></td>
            <td><%= Html.TextBox("username", null, new { @class = "userbox" })%></td>
        </tr>
        <tr>
            <td>
                <asp:Label ID="Label2" runat="server" Text="Password:"></asp:Label></td>
            <td><%= Html.TextBox("password", null, new { @class = "input-box" })%></td>
        </tr>
        <tr>
            <td><input class="button" name="Login" type="submit" value="Login" /></td>
            <td align="right" valign="bottom" />
        </tr>
    </table>
<% }  %>

This creates two labels and two fields to take our Username and Password plus a button. The Page looks something like this (depends on your site master)

Login Page

Wiring up Forms Authentication

That is most of the code, and now we are down to the last 100 yrds. The Provider has been written, the login screen created but the site is still not secure. Remaining is to wire up the Web.config of the client site to tell it which pages anyone can visit – and which ones will force a visit to the login page.

The wiring up occurs in the <system.web> tag of the web.config.

First off alter the <authentication> tag so that mode is set to “Forms”, and in the <forms> tag has the login url set to your login page. It should look something like this.

    <authentication mode="Forms">
      <forms loginUrl="~/Account/Logon" timeout="2880" />
    </authentication>

Following this we then block access to all pages in the website with an authorization, before granting access to just those we want to make public.

	  <authorization>
		  <deny users="?"/>
	  </authorization>

Now we need to tell it not to use the default membership provider, but to use our own provider. Just below the authorization tag place the following;

   <membership defaultProvider="SimplicitaMembershipProvider">
      <providers>
        <clear/>
		  <add name="SimplicitaMembershipProvider" type="Simplicita.Portal.Models.SimplicitaMembershipProvider, Simplicita.Portal"/>
      </providers>
    </membership>

Now when the application is run – it will start with the login page – however it will look terrible because amongst those things that were blocked by the authentication was the CSS and images folders where all the CSS and images for the site are kept – to grant access to these folders we now need to revoke the authentication block, for these areas only.

Just after the <system.web> tag place the following

	<location path="Content">
		<system.web>
			<authorization>
				<allow users="*" />
			</authorization>
		</system.web>
	</location>

	<location path="Content/images">
		<system.web>
			<authorization>
				<allow users="*" />
			</authorization>
		</system.web>
	</location>
	<location path="Content/Helpers">
		<system.web>
			<authorization>
				<allow users="*" />
			</authorization>
		</system.web>
	</location>

So that’s it – a secure MVC site, that uses our own Membership Provider.

Advertisements

7 thoughts on “Using a Custom Membership Provider in an MVC Application

  1. Pingback: 2010 in review « Developing My Cloud Application

  2. This is a good post, but can you please answer my question…

    I have an existing database with a users table, and we are planning to take the database and use it for a new system built in ASP.NET MVC. However, what I am uncertain about is whether or not I am able to create a login system that doesn’t use the built in account controller or a regular membership provider so that we can still use the existing table structure.

    So my question is, would this be possible? Or even particularly difficult to do if it is?

    What is the most widely accepted way of doing things and the simplest?

    • To be honest it depends. There is a lot provided out of the box, and there is no reason to recreate the wheel. That said, sometimes what is provided does not always fit with what you need.

      So yes, you can use the existing facilities, or roll parts that you need yourself. What you would need to do is replace ValidateUser with one that checks your database.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s