(..is of course not entirely true - but it does sound catchy, right ?)
Why eliminating forms authentication ?
Forms authentication is enabled by adding the <authentication mode="Forms"> tag to the web.config file. Within this tag, it also needs the hardcoded url for the login page (and optionally the default page). Furthermore, the FormsAuthentication class methods have to be used in the code-behind to SignOut(), RedirectFromLoginPage(), ..etc.
All in all, that's some very tightly coupled code, and extremely difficult to test. Ideally, we would like to have all authentication code in our business code (without adding reference to System.Web of course). I'll show you just how to do it, building upon the previously posted MvpTemplate sample code.
Building the mechanism
Suppose business requirements of our web application state that anonymous access is not allowed, and every visitor must be logged in. For sake of simplicity, in our example this means we will add a logon page where the user has to click a button "Logon" to be authenticated before he can view any other page. Once the button is clicked, a boolean property "IsLoggedOn" will be set on the session. In a real world application, a user object or something similar would be set on the session to indicate successful authentication.
When implementing, the first thing to do is define the contract (the ILogon view-interface) for the logon page. Since we are faking the authentication procedure of username/password checking with just a click of a button, the Ilogon is very straightforward and looks like :
1: public interface ILogon : IPageView
2: {
3: event EventHandler LogonClicked;
4: }
That was not too hard, was it ? Now we implement the presenter for this page :
1: public class LogonPresenter : BasePresenter<ILogon>
2: {
3: public LogonPresenter(ILogon view) : base(view)
4: {
5: }
6:
7: protected override void SubscribeViewToEvents()
8: {
9: View.LogonClicked += new EventHandler(View_LogonClicked);
10: }
11:
12: void View_LogonClicked(object sender, EventArgs e)
13: {
14: SessionComponent.IsLoggedIn = true;
15: NavigationComponent.NavigateTo(PageIdentifier.TestPage1);
16: }
17: }
So when the user clicks the "logon" button, the IsLoggedIn property on the session is set to true, and we navigate to the "default" page. (more on this later). Now all we need to do is a "catch-all" for all pages to redirect to the logon page when we are not yet logged in. Seems like our BasePresenter class would be most up to the task ! Let's refactor the constructor to :
1: protected BasePresenter(TView view)
2: {
3: View = view;
4: if (CheckAuthentication(view as IPageView))
5: {
6: View.Load += OnViewLoad;
7: SubscribeViewToEvents();
8: }
9: }
and add the CheckAuthentication method :
1: private bool CheckAuthentication(IPageView page)
2: {
3: if (page==null)//we have a control instead of page
4: return true;
5: if (page.PageIdentifier==PageIdentifier.LogonPage)
6: return true;
7: if (SessionComponent.IsLoggedIn)//yes, we are logged in !
8: return true;
9: //not logged in ! go to logonpage
10: NavigationComponent.NavigateTo(PageIdentifier.LogonPage);
11: return false;
12: }
Now, every page will check authentication (the check is skipped for controls, as you can never navigate directly to a control anyway), and redirect to the logon page if the user was not logged in.
Ofcourse, the logonpage's ASPX itself still needs to be implemented. Add a LogonPage.aspx to the web project, drag a button named 'ButtonLogon' onto the form, and implement the codebehind as follows:
1: public partial class LogonPage : BasePage,ILogon
2: {
3: protected override IPresenter CreatePresenter()
4: {
5: return new LogonPresenter(this);
6: }
7:
8: protected override void CreateEventhandlers()
9: {
10: ButtonLogon.Click += (s, e) => LogonClicked(s, e);
11: }
12:
13: public override PageIdentifier PageIdentifier
14: {
15: get { return PageIdentifier.LogonPage; }
16: }
17:
18: public event EventHandler LogonClicked;
19: }
Again, not that hard eh ;) The last thing to do is add the ASPX path to the PageIdentifier mapping in the NavigationComponent to enable that sweet strongly typed navigation. By now it should look like this :
Perfect ! A full working authentication module ... or is it ... ?
Adding some "user-friendliness"
What if a non-logged in user navigates directly to TestPage2 ? He get's redirected to the Logon page, and after logging in, he gets redirected to TestPage1. The ASP.NET forms authentication doesn't work quite that way, it would redirect to TestPage2, the original page. So let's built that into our solution too !
First we need some method on our NavigationComponent to redirect to another page, while also stating that we eventually will want to return. And we'll also need a method that actually does this return-redirect.
1: private const string ReturnUrl = "ReturnUrl";
2:
3: public void NavigateAndReturnTo(PageIdentifier pageIdentifier)
4: {
5: NavigateTo(pageIdentifier, ReturnUrl, HttpUtility.UrlEncode(HttpContext.Current.Request.RawUrl));
6: }
7:
8: public bool ReturnToCaller()
9: {
10: if (HttpContext.Current.Request.QueryString.AllKeys.Contains(ReturnUrl))
11: {
12: HttpContext.Current.Response.Redirect((HttpContext.Current.Request.QueryString[ReturnUrl]));
13: return true;
14: }
15: return false;
16: }
Note that we don't use the QueryParameter enum. There's no point in it, since we don't need the business layer to have knowledge of the implementation. In fact, we want to shield the businesslayer from this queryparameter to prevent tampering. So we define the name of the parameter within the NavigationComponent class itself.
Now don't forget to "pull members up" to put the method signatures in out INavigationComponent method, and then change the CheckAuthentication() in the BasePresenter to :
A little change is also needed on the LogonPresenter :
So now we have the ASP.NET forms authentication behavior. When accessing a page, we get redirected to the logon page, and after logging in, we come back to our original page. If we arrived at the logonpage with a direct request (not redirected from another page), we get forwarded to the "default" page instead after logging in.
The next step is the possibility to have "mixed" pages. Sometimes, you'll want to have a site with authentication, but on the other hand also some public pages. That's very easy to add. We could just add a "IsPublic" property to each page, but then that will be scattered all over the place, and you should know by now that i like to keep things centralized. So let's introduce a "SecurityComponent" class in our business layer.
We start by adding the interface :
1: public interface ISecurityCompnent
2: {
3: bool IsPublic(PageIdentifier pageIdentifier);
4: }
Since this security component will be accessed in the BasePresenter, let's also add an external depedency to (using the exdep snippet) :
1: private ISecurityComponent _SecurityComponent;
2: private ISecurityComponent SecurityComponent
3: {
4: get
5: {
6: if (_SecurityComponent == null)
7: _SecurityComponent = ServiceLocator.Instance().Resolve<ISecurityComponent>();
8: return _SecurityComponent;
9: }
10: }
Now, the implementation could de something like :
1: public class SecurityComponent : ISecurityComponent
2: {
3: private static readonly IList<PageIdentifier> _publicPages =
4: new List<PageIdentifier>()
5: {
6: PageIdentifier.LogonPage
7: };
8:
9: public bool IsPublic(PageIdentifier pageIdentifier)
10: {
11: return _publicPages.Contains(pageIdentifier);
12: }
13: }
so there we have it, strongly typed declaration of our public pages, centrally located in the SecurityComponent. Don't forget to add the SecurityComponent and it's interface to the ServiceLocator :
1: _container.RegisterType<ISecurityComponent, SecurityComponent>();
Now we can refactor the CheckAuthentication() method in our BasePresenter to :
Eliminating lines of code in baseclasses is always pleasant !
Now with the securitycomponent in place, the door is open to some more serious authorization checks, and I'll show you how to do this ...in a next post :)
In attach you'll find the updated code of the MvpWebTemplate example solution.
MvpWebTemplate.zip (578,19 kb)