SaaS CMS has officially launched! Learn more now.

Daniel Ovaska
Jun 15, 2016
  18602
(7 votes)

Creating a custom login page in Episerver and MVC

After playing around with some fancy pancy caching and AOP in earlier blog posts, I think it's time to return to the basics. Pretty often (today for instance) in Episerver world you hear someone ask how to create a custom login page. So I guess it's time for a blog bost on the subject. It's really easy and most is really standard .NET and MVC. I'll use the Alloy project as base project.

Let's start with the content type. One new LoginPage coming right up.

/// <summary>
/// Used for logging in on the website
/// </summary>
[SiteContentType(
    GroupName = Global.GroupNames.Specialized,
    GUID = "AEECADF2-3E89-4117-ADEB-F8D43565D2A8")]
[SiteImageUrl(Global.StaticGraphicsFolderPath + "page-type-thumbnail-article.png")]
public class LoginPage : StandardPage
{

}

Ok, that was easy...and not so much to look at to be honest. I also enabled it under the startpage content type so I could create one in edit mode below the start page with the url /en/login. Let's make a viewmodel that can accept some input such as username and password as well. I'll also include returnurl to add easy support for that.

public class LoginModel : PageViewModel<LoginPage>
{
    public LoginFormPostbackData LoginPostbackData { get; set; } = new LoginFormPostbackData();
    public LoginModel(LoginPage currentPage)
        : base(currentPage)
    {
    }
    public string Message { get; set; }
}

public class LoginFormPostbackData
{
    public string Username { get; set; }
    public string Password { get; set; }
    public bool RememberMe { get; set; }
    public string ReturnUrl { get; set; }
}

There we go! I used the standard alloy way and inherited the viewmodel from the PageViewModel generic class. This makes it easy to pass along the current page and get some Episerver magic without boring mappings. Let's take the controller next. This is where the magic happens. We'll need both an index action and a post action to handle the submit. If the returnurl querystring parameter isn't set, we'll use the default url specified in web.config.

public class LoginPageController : PageControllerBase<LoginPage>
{
    public ActionResult Index(LoginPage currentPage, [FromUri]string ReturnUrl)
    {
        var model = new LoginModel(currentPage);
        model.LoginPostbackData.ReturnUrl = ReturnUrl;
        return View(model);
    }
    [System.Web.Mvc.HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Post(LoginPage currentPage,[FromBody] LoginFormPostbackData LoginPostbackData)
    {
        var model = new LoginModel(currentPage);
        var isValid = Membership.Provider.ValidateUser(LoginPostbackData.Username, LoginPostbackData.Password);
        if (isValid)
        {
            var redirectUrl = GetRedirectUrl(LoginPostbackData.ReturnUrl);
            FormsAuthentication.SetAuthCookie(LoginPostbackData.Username, LoginPostbackData.RememberMe);
            return Redirect(redirectUrl); //Important to redirect after login to be sure cookies etc are set.
        }
        model.Message = "Wrong credentials, try again";
        return View("Index",model);
    }
    /// <summary>
    /// You can extend this to set redirect url to some property you set on login page in edit if you like
    /// Might also depend on role of user...
    /// </summary>
    public string GetRedirectUrl(string returnUrl)
    {
        if (!string.IsNullOrEmpty(returnUrl))
        {
            return returnUrl;
        }
        return FormsAuthentication.DefaultUrl;
    }
}

I added the ValidateAntiForgeryToken to increase security and avoid CSRF attacks

Notice I use the standard .NET Membership provider to validate users and the standard .NET FormsAuthentication to redirect users again. Avoid writing your own authentication/authorization logic here for security reasons. Use the standard .NET variants and hook into these instead like above. Always redirect after you are done to make sure you reset all cookies etc.

Let's add a view to this as well to finish this baby!

@using EPiServer.Globalization
@using EPiServerSite9TestSite
@model LoginModel

@{ Layout = "~/Views/Shared/Layouts/_LeftNavigation.cshtml"; }

<h1 @Html.EditAttributes(x => x.CurrentPage.PageName)>@Model.CurrentPage.PageName</h1>
<p class="introduction" @Html.EditAttributes(x => x.CurrentPage.MetaDescription)>@Model.CurrentPage.MetaDescription</p>
<div class="row">
    <div class="span8 clearfix" @Html.EditAttributes(x => x.CurrentPage.MainBody)>
        @Html.DisplayFor(m => m.CurrentPage.MainBody)

    </div>

</div>
<div class="row">
        @using (Html.BeginForm("Post", null, new { language = ContentLanguage.PreferredCulture.Name }))
            {
            <div class="logo"></div>
                @Html.AntiForgeryToken()
                <h2 class="form-signin-heading">Log in</h2>
                @Html.LabelFor(m => m.LoginPostbackData.Username, new { @class = "sr-only" })
                @Html.TextBoxFor(m => m.LoginPostbackData.Username, new { @class = "form-control", autofocus = "autofocus" })

                @Html.LabelFor(m => m.LoginPostbackData.Password, new { @class = "sr-only" })
                @Html.PasswordFor(m => m.LoginPostbackData.Password, new { @class = "form-control" })
                <div class="checkbox">
                    <label>
                        @Html.CheckBoxFor(m => m.LoginPostbackData.RememberMe)
                        @Html.DisplayNameFor(m => m.LoginPostbackData.RememberMe)
                    </label>
                </div>

                @Html.HiddenFor(m => m.LoginPostbackData.ReturnUrl)
                <input type="submit" value="Log in" class="btn btn-lg btn-primary btn-block" />
        }
        @Html.DisplayFor(m => m.Message)
</div>

@Html.PropertyFor(x => x.CurrentPage.Link)
@Html.PropertyFor(x => x.CurrentPage.Links)
@Html.PropertyFor(x => x.CurrentPage.MainContentArea, new { CssClass = "row", Tag = Global.ContentAreaTags.TwoThirdsWidth })

There. Almost done. I'll also change the default login page for Episerver.This is done in web.config

<authentication mode="Forms">
  <forms name=".EPiServerLogin" loginUrl="/en/login" timeout="120" defaultUrl="~/" />
</authentication>

What is left to do? I'll leave adding some validation messages, logging, multiple start pages etc for now to keep it clean. If you are more interested in securing your website you can read more here. I hope this will help you get started on your own login pages.

Happy coding!

Jun 15, 2016

Comments

Jay Burling
Jay Burling Jan 22, 2017 09:07 PM

This article was fantastically helpful at understanding how to build a custom login page, as well as showing a great example of handling user posted data and use of the provider model!

One tiny, tiny note: My AlloyDemo site didn't have the Web API components that the controller needed to work (the [FromURI] and [FromBody] attributes) and I had to go grab them off NuGet to get everything running smoothly.

Again, very helpful article!

Chaudhry Mohsin Ali
Chaudhry Mohsin Ali Oct 31, 2017 12:43 PM

I am trying this thing and it doesn't work. 

public ActionResult Index(LoginPage currentPage, [FromUri]string ReturnUrl)
        {

I am always getting currentPage and ReturnUri as null

Chaudhry Mohsin Ali
Chaudhry Mohsin Ali Nov 2, 2017 11:44 AM

Hello,

How did you enabled it under the start page? 

arati shinde
arati shinde Dec 13, 2017 07:53 AM

 very helpful article!laughing thanks

Dec 13, 2017 01:53 PM

@Chaudhry To enable the new page type you can go to the class you have for the start page and edit the attribute for available page types. Add the LoginPage like:

[AvailablePageTypes(Include = new Type[] { typeof(LoginPage) })]


Bartosz Sekula
Bartosz Sekula Apr 27, 2018 05:17 PM

If anyone is interested in making it work with OWIN then please take a look at my blog post.

https://bartoszsekula.com/post/2018/04/27/how-to-create-a-custom-login-page-in-episerver-mvc-with-owin

You will find a link to github together with a working sample.

Please login to comment.
Latest blogs
Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024

Optimizely London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Configure local development - Part 2

This is part 2 of a proof-of-concept (POC) blog. In this post, I will guide you through the steps to configure your SaaS instance with your local...

Raghavendra Murthy | Jul 19, 2024 | Syndicated blog