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
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
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.
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
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="Data Source=MYSERVER\SQLEXPRESS;Initial Catalog=Simplicita;Integrated Security=True;MultipleActiveResultSets=True"" 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);
}
}
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.
The Model
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
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)
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.






That’s an amazing post. Thanks a lot
Please, put the link to download the app.
This is part of a much larger site and this is really a guide (I did not code most of the properties of the MembershipProvider for example). I don’t really have a sample app as such. If you have any problems don’t hesitate to ask.
Pingback: 2010 in review « Developing My Cloud Application