Create a secure ASP.NET Web Forms app with user registration, email confirmation and password reset (C#)

by Erik Reitan

This tutorial shows you how to build an ASP.NET Web Forms app with user registration, email confirmation and password reset using the ASP.NET Identity membership system. This tutorial was based on Rick Anderson's MVC tutorial.

Introduction

This tutorial guides you through the steps required to create an ASP.NET Web Forms application using Visual Studio and ASP.NET 4.5 to create a secure Web Forms app with user registration, email confirmation and password reset.

Tutorial Tasks and Information:

Create an ASP.NET Web Forms App

Note

Warning: You must install Visual Studio 2013 Update 3 or higher to complete this tutorial.

  1. Create a new project (File -> New Project) and select the ASP.NET Web Application template and the latest .NET Framework version from the New Project dialog box.

  2. From the New ASP.NET Project dialog box, select the Web Forms template. Leave the default authentication as Individual User Accounts. If you'd like to host the app in Azure, leave the Host in the cloud check box checked.
    Then, click OK to create the new project.
    New ASP.NET Project dialog box

  3. Enable Secure Sockets Layer (SSL) for the project. Follow the steps available in the Enable SSL for the Project section of the Getting Started with Web Forms tutorial series.

  4. Run the app, click the Register link and register a new user. At this point, the only validation on the email is based on the [EmailAddress] attribute to ensure the email address is well-formed. You will modify the code to add email confirmation. Close the browser window.

  5. In Server Explorer of Visual Studio (View -> Server Explorer), navigate to Data Connections\DefaultConnection\Tables\AspNetUsers, right click and select Open table definition.

    The following image shows the AspNetUsers table schema:

    AspNetUsers table schema

  6. In Server Explorer, right-click on the AspNetUsers table and select Show Table Data.

    AspNetUsers table data
    At this point the email for the registered user has not been confirmed.

  7. Click on the row and select delete to delete the user. You'll add this email again in the next step and send a confirmation message to the email address.

Email Confirmation

It's a best practice to confirm the email during the registration of a new user to verify they are not impersonating someone else (that is, they haven't registered with someone else's email). Suppose you had a discussion forum, you would want to prevent "bob@cpandl.com" from registering as "joe@contoso.com". Without email confirmation, "joe@contoso.com" could get unwanted email from your app. Suppose Bob accidentally registered as "bib@cpandl.com" and hadn't noticed it, he wouldn't be able to use password recovery because the app doesn't have his correct email. Email confirmation provides only limited protection from bots and doesn't provide protection from determined spammers.

You generally want to prevent new users from posting any data to your website before they have been confirmed by either email, an SMS text message or another mechanism. In the sections below, we will enable email confirmation and modify the code to prevent newly registered users from logging in until their email has been confirmed. You'll use the email service SendGrid in this tutorial.

Hook up SendGrid

SendGrid has changed it's API since this tutorial was written. For current SendGrid instructions, see SendGrid or Enable account confirmation and password recovery.

Although this tutorial only shows how to add email notification through SendGrid, you can send email using SMTP and other mechanisms (see additional resources).

  1. In Visual Studio, open the Package Manager Console (Tools -> NuGet Package Manger -> Package Manager Console), and enter the following command:
    Install-Package SendGrid

  2. Go to the Azure SendGrid sign-up page and register for free SendGrid account. You can also sign-up for a free SendGrid account directly on SendGrid's site.

  3. From Solution Explorer open the IdentityConfig.cs file in the App_Start folder and add the following code highlighted in yellow to the EmailService class to configure SendGrid:

    public class EmailService : IIdentityMessageService
    {
       public async Task SendAsync(IdentityMessage message)
       {
         await configSendGridasync(message);
       }
    
       // Use NuGet to install SendGrid (Basic C# client lib) 
       private async Task configSendGridasync(IdentityMessage message)
       {
          var myMessage = new SendGridMessage();
          myMessage.AddTo(message.Destination);
          myMessage.From = new System.Net.Mail.MailAddress(
                              "Royce@contoso.com", "Royce Sellars (Contoso Admin)");
          myMessage.Subject = message.Subject;
          myMessage.Text = message.Body;
          myMessage.Html = message.Body;
    
          var credentials = new NetworkCredential(
                     ConfigurationManager.AppSettings["emailServiceUserName"],
                     ConfigurationManager.AppSettings["emailServicePassword"]
                     );
    
          // Create a Web transport for sending email.
          var transportWeb = new Web(credentials);
    
          // Send the email.
          if (transportWeb != null)
          {
             await transportWeb.DeliverAsync(myMessage);
          }
          else
          {
             Trace.TraceError("Failed to create Web transport.");
             await Task.FromResult(0);
          }
       }
    }
    
  4. Also, add the following using statements to the beginning of the IdentityConfig.cs file:

    using SendGrid;
    using System.Net;
    using System.Configuration;
    using System.Diagnostics;
    
  5. To keep this sample simple, you'll store the email service account values in the appSettings section of the web.config file. Add the following XML highlighted in yellow to the web.config file at the root of your project:

    </connectionStrings>
       <appSettings>
          <add key="emailServiceUserName" value="[EmailServiceAccountUserName]" />
          <add key="emailServicePassword" value="[EmailServiceAccountPassword]" />
       </appSettings>
      <system.web>
    

    Warning

    Security - Never store sensitive data in your source code. In this example, the account and credentials are stored in the appSetting section of the Web.config file. On Azure, you can securely store these values on the Configure tab in the Azure portal. For related information see Rick Anderson's topic titled Best practices for deploying passwords and other sensitive data to ASP.NET and Azure.

  6. Add the email service values to reflect your SendGrid authentication values (User Name and Password) so that you can successful send email from your app. Be sure to use your SendGrid account name rather than the email address you provided SendGrid.

Enable Email Confirmation

To enable email confirmation, you'll modify the registration code using the following steps.

  1. In the Account folder, open the Register.aspx.cs code-behind and update the CreateUser_Click method to enable the following highlighted changes:

    protected void CreateUser_Click(object sender, EventArgs e)
    {
        var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var user = new ApplicationUser() { UserName = Email.Text, Email = Email.Text };
        IdentityResult result = manager.Create(user, Password.Text);
        if (result.Succeeded)
        {
            // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
            string code = manager.GenerateEmailConfirmationToken(user.Id);
            string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);
            manager.SendEmail(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>.");
    
            IdentityHelper.SignIn(manager, user, isPersistent: false);
            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
        }
        else 
        {
            ErrorMessage.Text = result.Errors.FirstOrDefault();
        }
    }
    
  2. In Solution Explorer, right-click Default.aspx and select Set As Start Page.

  3. Run the app by pressing F5. After the page is displayed, click the Register link to display the Register page.

  4. Enter your email and password, then click the Register button to send an email message via SendGrid.
    The current state of your project and code will allow the user to log in once they complete the registration form, even though they haven't confirmed their account.

  5. Check your email account and click on the link to confirm your email.
    Once you submit the registration form, you will be logged in.
    Sample Website - Signed In

Require Email Confirmation Before Log In

Although you have confirmed the email account, at this point you would not need to click on the link contained in the verification email to be fully signed-in. In the following section, you will modify the code requiring new users to have a confirmed email before they are logged in (authenticated).

  1. In Solution Explorer of Visual Studio, update the CreateUser_Click event in the Register.aspx.cs code-behind contained in the Accounts folder with the following highlighted changes:

    protected void CreateUser_Click(object sender, EventArgs e)
    {
        var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var user = new ApplicationUser() { UserName = Email.Text, Email = Email.Text };
        IdentityResult result = manager.Create(user, Password.Text);
        if (result.Succeeded)
        {
            // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
            string code = manager.GenerateEmailConfirmationToken(user.Id);
            string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);
            manager.SendEmail(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>.");
    
            if (user.EmailConfirmed)
            {
              IdentityHelper.SignIn(manager, user, isPersistent: false);
              IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
            } 
            else
            {
              ErrorMessage.Text = "An email has been sent to your account. Please view the email and confirm your account to complete the registration process.";
            }
        }
        else 
        {
            ErrorMessage.Text = result.Errors.FirstOrDefault();
        }
    }
    
  2. Update the LogIn method in the Login.aspx.cs code-behind with the following highlighted changes:

    protected void LogIn(object sender, EventArgs e)
    {
        if (IsValid)
        {
            // Validate the user password
            var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
            var signinManager = Context.GetOwinContext().GetUserManager<ApplicationSignInManager>();
    
            // Require the user to have a confirmed email before they can log on.
            var user = manager.FindByName(Email.Text);
            if (user != null)
            {
                if (!user.EmailConfirmed)
                {
                    FailureText.Text = "Invalid login attempt. You must have a confirmed email account.";
                    ErrorMessage.Visible = true;
                }
                else
                {
                    // This doen't count login failures towards account lockout
                    // To enable password failures to trigger lockout, change to shouldLockout: true
                    var result = signinManager.PasswordSignIn(Email.Text, Password.Text, RememberMe.Checked, shouldLockout: false);
    
                    switch (result)
                    {
                        case SignInStatus.Success:
                            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
                            break;
                        case SignInStatus.LockedOut:
                            Response.Redirect("/Account/Lockout");
                            break;
                        case SignInStatus.RequiresVerification:
                            Response.Redirect(String.Format("/Account/TwoFactorAuthenticationSignIn?ReturnUrl={0}&RememberMe={1}",
                                                        Request.QueryString["ReturnUrl"],
                                                        RememberMe.Checked),
                                          true);
                            break;
                        case SignInStatus.Failure:
                        default:
                            FailureText.Text = "Invalid login attempt";
                            ErrorMessage.Visible = true;
                            break;
                    }
                }
            }
        }          
    }
    

Run the Application

Now that you have implemented the code to check whether a user's email address has been confirmed, you can check the functionality on both the Register and Login pages.

  1. Delete any accounts in the AspNetUsers table that contain the email alias you wish to test.
  2. Run the app (F5) and verify you cannot register as a user until you have confirmed your email address.
  3. Before confirming your new account via the email that was just sent, attempt to log in with the new account.
    You'll see that you are unable to log in and that you must have a confirmed email account.
  4. Once you confirm your email address, log in to the app.

Password Recovery and Reset

  1. In Visual Studio, remove the comment characters from the Forgot method in the Forgot.aspx.cs code-behind contained in the Account folder, so that the method appears as follows:

    protected void Forgot(object sender, EventArgs e)
    {
        if (IsValid)
        {
            // Validate the user's email address
            var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
            ApplicationUser user = manager.FindByName(Email.Text);
            if (user == null || !manager.IsEmailConfirmed(user.Id))
            {
                FailureText.Text = "The user either does not exist or is not confirmed.";
                ErrorMessage.Visible = true;
                return;
            }
            // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=320771
            // Send email with the code and the redirect to reset password page
            string code = manager.GeneratePasswordResetToken(user.Id);
            string callbackUrl = IdentityHelper.GetResetPasswordRedirectUrl(code, Request);
            manager.SendEmail(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>.");
            loginForm.Visible = false;
            DisplayEmail.Visible = true;
        }
    }
    
  2. Open the Login.aspx page. Replace the markup near the end of the loginForm section as highlighted below:

    <%@ Page Title="Log in" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="WebForms002.Account.Login" Async="true" %>
    
    <%@ Register Src="~/Account/OpenAuthProviders.ascx" TagPrefix="uc" TagName="OpenAuthProviders" %>
    
    <asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
        <h2><%: Title %>.</h2>
    
        <div class="row">
            <div class="col-md-8">
                <section id="loginForm">
                    <div class="form-horizontal">
                        <h4>Use a local account to log in.</h4>
                        <hr />
                        <asp:PlaceHolder runat="server" ID="ErrorMessage" Visible="false">
                            <p class="text-danger">
                                <asp:Literal runat="server" ID="FailureText" />
                            </p>
                        </asp:PlaceHolder>
                        <div class="form-group">
                            <asp:Label runat="server" AssociatedControlID="Email" CssClass="col-md-2 control-label">Email</asp:Label>
                            <div class="col-md-10">
                                <asp:TextBox runat="server" ID="Email" CssClass="form-control" TextMode="Email" />
                                <asp:RequiredFieldValidator runat="server" ControlToValidate="Email"
                                    CssClass="text-danger" ErrorMessage="The email field is required." />
                            </div>
                        </div>
                        <div class="form-group">
                            <asp:Label runat="server" AssociatedControlID="Password" CssClass="col-md-2 control-label">Password</asp:Label>
                            <div class="col-md-10">
                                <asp:TextBox runat="server" ID="Password" TextMode="Password" CssClass="form-control" />
                                <asp:RequiredFieldValidator runat="server" ControlToValidate="Password" CssClass="text-danger" ErrorMessage="The password field is required." />
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <div class="checkbox">
                                    <asp:CheckBox runat="server" ID="RememberMe" />
                                    <asp:Label runat="server" AssociatedControlID="RememberMe">Remember me?</asp:Label>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <asp:Button runat="server" OnClick="LogIn" Text="Log in" CssClass="btn btn-default" />
                            </div>
                        </div>
                    </div>
                    <p>
                        <asp:HyperLink runat="server" ID="RegisterHyperLink" ViewStateMode="Disabled">Register as a new user</asp:HyperLink>
                    </p>
                    <p>
                        <%-- Enable this once you have account confirmation enabled for password reset functionality --%>
                        <asp:HyperLink runat="server" ID="ForgotPasswordHyperLink" ViewStateMode="Disabled">Forgot your password?</asp:HyperLink>
                    </p>
                </section>
            </div>
    
            <div class="col-md-4">
                <section id="socialLoginForm">
                    <uc:OpenAuthProviders runat="server" ID="OpenAuthLogin" />
                </section>
            </div>
        </div>
    </asp:Content>
    
  3. Open the Login.aspx.cs code-behind and uncomment the following line of code highlighted in yellow from the Page_Load event handler:

    protected void Page_Load(object sender, EventArgs e)
    {
        RegisterHyperLink.NavigateUrl = "Register";
        // Enable this once you have account confirmation enabled for password reset functionality
        ForgotPasswordHyperLink.NavigateUrl = "Forgot";
        OpenAuthLogin.ReturnUrl = Request.QueryString["ReturnUrl"];
        var returnUrl = HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"]);
        if (!String.IsNullOrEmpty(returnUrl))
        {
            RegisterHyperLink.NavigateUrl += "?ReturnUrl=" + returnUrl;
        }
    }
    
  4. Run the app by pressing F5. After the page is displayed, click the Log in link.

  5. Click the Forgot your password? link to display the Forgot Password page.

  6. Enter your email address and click the Submit button to send an email to your address which will allow you to reset your password.
    Check your email account and click on the link to display the Reset Password page.

  7. On the Reset Password page, enter your email, password, and confirmed password. Then, press the Reset button.
    When you successfully reset your password, the Password Changed page will be displayed. Now you can log in with your new password.

Once a user creates a new local account, they are emailed a confirmation link they are required to use before they can log on. If the user accidentally deletes the confirmation email, or the email never arrives, they will need the confirmation link sent again. The following code changes show how to enable this.

  1. In Visual Studio, open the Login.aspx.cs code-behind and add the following event handler after the LogIn event handler:

    protected void SendEmailConfirmationToken(object sender, EventArgs e)
    {
        var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var user = manager.FindByName(Email.Text);
        if (user != null)
        {
            if (!user.EmailConfirmed)
            {
                string code = manager.GenerateEmailConfirmationToken(user.Id);
                string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);
                manager.SendEmail(user.Id, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>.");
    
                FailureText.Text = "Confirmation email sent. Please view the email and confirm your account.";
                ErrorMessage.Visible = true;
                ResendConfirm.Visible = false;
            }
        }
    }
    
  2. Modify the LogIn event handler in the Login.aspx.cs code-behind by changing the code highlighted in yellow as follows:

    protected void LogIn(object sender, EventArgs e)
    {
        if (IsValid)
        {
            // Validate the user password
            var manager = Context.GetOwinContext().GetUserManager<ApplicationUserManager>();
            var signinManager = Context.GetOwinContext().GetUserManager<ApplicationSignInManager>();
    
            // Require the user to have a confirmed email before they can log on.
            var user = manager.FindByName(Email.Text);
            if (user != null)
            {
                if (!user.EmailConfirmed)
                {
                    FailureText.Text = "Invalid login attempt. You must have a confirmed email address. Enter your email and password, then press 'Resend Confirmation'.";
                    ErrorMessage.Visible = true;
                    ResendConfirm.Visible = true;
                }
                else
                {
                    // This doen't count login failures towards account lockout
                    // To enable password failures to trigger lockout, change to shouldLockout: true
                    var result = signinManager.PasswordSignIn(Email.Text, Password.Text, RememberMe.Checked, shouldLockout: false);
    
                    switch (result)
                    {
                        case SignInStatus.Success:
                            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
                            break;
                        case SignInStatus.LockedOut:
                            Response.Redirect("/Account/Lockout");
                            break;
                        case SignInStatus.RequiresVerification:
                            Response.Redirect(String.Format("/Account/TwoFactorAuthenticationSignIn?ReturnUrl={0}&RememberMe={1}",
                                                        Request.QueryString["ReturnUrl"],
                                                        RememberMe.Checked),
                                          true);
                            break;
                        case SignInStatus.Failure:
                        default:
                            FailureText.Text = "Invalid login attempt";
                            ErrorMessage.Visible = true;
                            break;
                    }
                }
            }
        }
    }
    
  3. Update the Login.aspx page by adding the code highlighted in yellow as follows:

    <%@ Page Title="Log in" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="WebForms002.Account.Login" Async="true" %>
    
    <%@ Register Src="~/Account/OpenAuthProviders.ascx" TagPrefix="uc" TagName="OpenAuthProviders" %>
    
    <asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
        <h2><%: Title %>.</h2>
    
        <div class="row">
            <div class="col-md-8">
                <section id="loginForm">
                    <div class="form-horizontal">
                        <h4>Use a local account to log in.</h4>
                        <hr />
                        <asp:PlaceHolder runat="server" ID="ErrorMessage" Visible="false">
                            <p class="text-danger">
                                <asp:Literal runat="server" ID="FailureText" />
                            </p>
                        </asp:PlaceHolder>
                        <div class="form-group">
                            <asp:Label runat="server" AssociatedControlID="Email" CssClass="col-md-2 control-label">Email</asp:Label>
                            <div class="col-md-10">
                                <asp:TextBox runat="server" ID="Email" CssClass="form-control" TextMode="Email" />
                                <asp:RequiredFieldValidator runat="server" ControlToValidate="Email"
                                    CssClass="text-danger" ErrorMessage="The email field is required." />
                            </div>
                        </div>
                        <div class="form-group">
                            <asp:Label runat="server" AssociatedControlID="Password" CssClass="col-md-2 control-label">Password</asp:Label>
                            <div class="col-md-10">
                                <asp:TextBox runat="server" ID="Password" TextMode="Password" CssClass="form-control" />
                                <asp:RequiredFieldValidator runat="server" ControlToValidate="Password" CssClass="text-danger" ErrorMessage="The password field is required." />
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <div class="checkbox">
                                    <asp:CheckBox runat="server" ID="RememberMe" />
                                    <asp:Label runat="server" AssociatedControlID="RememberMe">Remember me?</asp:Label>
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-offset-2 col-md-10">
                                <asp:Button runat="server" OnClick="LogIn" Text="Log in" CssClass="btn btn-default" />
                                &nbsp;&nbsp;
                                <asp:Button runat="server" ID="ResendConfirm"  OnClick="SendEmailConfirmationToken" Text="Resend confirmation" Visible="false" CssClass="btn btn-default" />
                            </div>
                        </div>
                    </div>
                    <p>
                        <asp:HyperLink runat="server" ID="RegisterHyperLink" ViewStateMode="Disabled">Register as a new user</asp:HyperLink>
                    </p>
                    <p>
                        <%-- Enable this once you have account confirmation enabled for password reset functionality --%>
                        <asp:HyperLink runat="server" ID="ForgotPasswordHyperLink" ViewStateMode="Disabled">Forgot your password?</asp:HyperLink>
                    </p>
                </section>
            </div>
    
            <div class="col-md-4">
                <section id="socialLoginForm">
                    <uc:OpenAuthProviders runat="server" ID="OpenAuthLogin" />
                </section>
            </div>
        </div>
    </asp:Content>
    
  4. Delete any accounts in the AspNetUsers table that contain the email alias you wish to test.

  5. Run the app (F5) and register your email address.

  6. Before confirming your new account via the email that was just sent, attempt to log in with the new account.
    You'll see that you are unable to log in and that you must have a confirmed email account. In addition, you can now resend a confirmation message to your email account.

  7. Enter your email address and password, then press the Resend confirmation button.

  8. Once you confirm your email address based on the newly sent email message, log in to the app.

Troubleshooting the App

If you don't receive an email containing the link to verify your credentials:

  • Check your junk or spam folder.
  • Log into your SendGrid account and click on the Email Activity link.
  • Be certain you used your SendGrid user account name as a Web.config value rather than your SendGrid account email address.

Additional Resources