Tuesday 19 October 2010

Experimenting with MVC and the Entity Framework – Part 4 - Authentication and Authorisation

Authentication and authorisation.

.Authentication = Who is it.

Authorisation = can they do it.

So – reading through the NerDinner section on authorisation and authentication then it looks like I can use good old FORMSAuth and APSNETDB to provide authentication and authorisation store out of the box.

I’ve been using these for a good few years now so don’t expect any issues.

I may change this later to:

o use a STS (Security Token Service) such as the excellent thinktecture Starter STS so I can secure my web services to a WS standard using the same backend store.

o Allow authentication from other systems such as facebook, google etc.

[more]

So I change my web.config to give a different name to the database as I know in my production database I have a few of these already! (Actually the database does support multiple applications but its much more difficult to migrate and maintain applications if they all use the same database).

<add name="ApplicationServices" connectionString="data source=.\SQL2008EXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb_victor.mdf;User Instance=true" providerName="System.Data.SqlClient" />

Ran the app and went to the register page.

clip_image002[9]

Database was created for me – worked a treat!

clip_image004[10]As you can see it now knows who I am!

Logged off and tried to add a comment – as expected it took me to the login screen. I had forgotten my password already so I was registered again.

I was not taken to the add comment screen – where I was originally heading.

I could not get my password back!

I’ll tackle the password retrieval first.

Password retrieval

Change the web.config to force unique emails and allow password reset:

<add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="ApplicationServices" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" applicationName="/" />

And then I added a class into the existing account model page:

public class PasswordRetrieveModel

{

[Required]

[DisplayName("User name or Email address")]

public string UserName { get; set; }

}

This class models the attributes required to retrieve our password.

I then added in a strongly typed view to show the details:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<victor.Models.PasswordRetrieveModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">

RetrievePassword

</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>RetrievePassword</h2>

<% using (Html.BeginForm()) { %>

<%= Html.ValidationSummary(true,"The password could not be reset") %>

<fieldset>

<legend>Fields</legend>

<div class="editor-label">

<%= Html.LabelFor(m => m.UserName) %>

</div>

<div class="editor-field">

<%= Html.TextBoxFor(m => m.UserName) %>

<%= Html.ValidationMessageFor(m => m.UserName) %>

</div>

</fieldset>

<p>

<input type="submit" value="RetrievePassword" />

</p>

<% } %>

</asp:Content>

and edited the model adding in password reset methods on the IMembershipService interface:

/// <summary>

/// reset the password

/// </summary>

/// <param name="p">username or email address</param>

/// <returns>flag indicating if password sent to the user.</returns>

bool ResetPassword(string p);

And an implementation on the AccountMembershipService class:

public class AccountMembershipService : IMembershipService

{

private static String rawMessage = @"You have requested a password reset for {0}.

Your username is: {1}

Your new password is: {2} ";

public bool ResetPassword(String p)

{

bool ret = false;

// first find the user by username or email address.

String username = null;

MembershipUser user = _provider.GetUser(p, true);

if (null == user)

{

username = _provider.GetUserNameByEmail(p);

if (null != username)

{

user = _provider.GetUser(username, true);

}

}

else

{

username = user.UserName;

}

// if we've found a user then send out his (reset) password

if (null != username)

{

user.UnlockUser();

String newpass = _provider.ResetPassword(username, null);

SmtpClient mailClient = new SmtpClient();

MailAddress address = new MailAddress(user.Email);

String message = String.Format(rawMessage,

HttpContext.Current.Request.Url.Host.ToString(),

user.UserName,

newpass );

mailClient.Send(ConfigurationSettings.AppSettings["fromemail"],

user.Email,

"Password Reset",

message);

ret = true;

}

return ret;

}

}

Finally I added new methods into the account controller:

// *****************************************

// URL: Password Request

// *****************************************

public ActionResult RetrievePassword()

{

return View();

}

[HttpPost]

public ActionResult RetrievePassword(PasswordRetrieveModel model)

{

ActionResult ret = null;

if (MembershipService.ResetPassword(model.UserName))

{

return View("PasswordRetrieved");

}

else

{

ModelState.AddModelError("", "The user was not found using the given Username or EMail address.");

ret = View();

}

return ret;

}

And added a section in the web.config to configure the smtp service.

<system.net>

<mailSettings>

<smtp deliveryMethod="Network">

<network host="localhost"/>

</smtp>

</mailSettings>

I usually set my local smtp service to allow relaying and relay to my real smtp server or just examine the mail in the iis dirctories where its droped (C:\Inetpub\mailroot\Queue ) to test. That way when I deploy to production (where the relaying works) I don’t need to change anything.

The relaying properties can be found in the Access tab of the smtp server properties in inetmgr.

clip_image006[8]

clip_image008[8]

Redirecting after registration

First thing to do is to pass the url to the registration page as part of the query string.

Please enter your username and password. <%= Html.ActionLink("Register", "Register", new { ReturnURL = Request.QueryString["ReturnURL"] })%> if you don't have an account.

I have to say at this point I have no idea how this works! Specifically the new { ReturnURL = Request.QueryString["ReturnURL"] } – how is the ReturnURL type declared?

The help says

“routeValues

Type: System..::.Object
An object that contains the parameters for a route. The parameters are retrieved through reflection by examining the properties of the object. The object is typically created by using object initializer syntax.

I read up on object initializer syntax on msdn but still don’t understand how the type of ReturnURL is not being delcared. Maybe the compiler is now smart eneugh to work out the type?

However – it does appear in the query string and so I can ammend the register method to take and act on this paramater–

[HttpPost]

public ActionResult Register(RegisterModel model, string returnUrl)

{

if (ModelState.IsValid)

{

// Attempt to register the user

MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email);

if (createStatus == MembershipCreateStatus.Success)

{

FormsService.SignIn(model.UserName, false /* createPersistentCookie */);

if (!String.IsNullOrEmpty(returnUrl))

{

return Redirect(returnUrl);

}

else

{

return RedirectToAction("Index", "Home");

}

}

else

{

ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus));

}

}

OK – pretty much all core functionality is there now. I just need to add venue matching – so we don’t duplicate venues, tidy up the screens with some help and put them in the right places, then, hopefully, I’ll have something to put live!

No comments:

Post a Comment