Adding Forms Authentication to Web API

When you create a new Web API project, it grafts the API onto the existing web project. This can prove sub-optimal in cases where you want everything (including Forms authentication) to call into the API.

Thankfully, it’s fairly easy to separate out the Web API code into a separate project, and then graft the authentication mechanism into it. (Ask yourself first if you really want to do this; the Web API starter project includes external authentication only by default).

Whether you want to use forms authentication on a single project with both the API and MVC code, or on a Web API project that’s separate from the MVC project, the steps are similar.

Caveat: I derived these changes by reverse-engineering an MVC5 application with Web API 2 — I started copying over code, then dependent code, and then adding missing config. These steps may change with newer versions of Web API.

Change the Authentication in WebApiConfig

Replace this block:

// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

With this line:

config.Filters.Add(new AuthorizeAttribute());

Add the ApplicationSignInManager to IdentityConfig

Copy/paste the ApplicationSignInManager class from your MVC project into App_Start/IdentityConfig.cs. Here’s how that looks today:

    // Configure the application sign-in manager which is used in this application.
    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
            : base(userManager, authenticationManager)
        {
        }

        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }

        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
        {
            return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
        }
    }

Enable Cookie-Based Authentication

Open up App_Start/Startup.Auth.cs and replace this line:

app.UseCookieAuthentication(new CookieAuthenticationOptions());

With this block:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.  
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});

Modify ApplicationUser’s GenerateUserIdentityAsync method

Open up Models/IdentityModel.cs and add a second version of GenerateUserIdentityAsync that takes only a single input:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
    return await this.GenerateUserIdentityAsync(manager, DefaultAuthenticationTypes.ApplicationCookie);
}

Add a View Model

Create a LoginViewModel in Models/AccountViewModels.cs:

public class LoginViewModel
{
    public string Email { get; set; }
    public string Password { get; set; }
    public bool RememberMe { get; set; }
}

Add a Login Method to your AccountController

Now, the bulk of the work: create the actual LogIn method on your AccountController. Conceptually, you need to add the SignInManager, and then wire up the call (just like the MVC project does).

First, add the SignInManager and wrapper property:

private ApplicationSignInManager _signInManager;

// ...

protected ApplicationSignInManager SignInManager
{
    get
    {
        return _signInManager ?? HttpContext.Current.GetOwinContext().Get<ApplicationSignInManager>();
    }
    private set
    {
        _signInManager = value;
    }
}

Next, update the constructor to take in this additional dependency (constructor injection):

public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager,
    ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
    UserManager = userManager;
    SignInManager = signInManager;
    AccessTokenFormat = accessTokenFormat;
}

I think this step is optional. There’s no code to call the constructor directly, and I didn’t see any dependency injection configuration. If you look closely at the SignInManager property from the last call, you’ll notice that it initializes itself.

Finally, wire up the LogIn call:

// POST Account/Login
[AllowAnonymous]
[Route("LogIn")]
public async Task<IHttpActionResult> LogIn(LoginViewModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var user = UserManager.FindByEmail(model.Email);

    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    if (result == SignInStatus.Success)
    {
        return Ok();
    }
    else
    {
        return BadRequest(ModelState);
    }
}

And that’s it! Now, you can log in (through code, or with an HTTP client) by POSTing your request. (eg. you can use RestSharp to create a client, POST a register method, and then POST a login call).

If you want to wire up your MVC5 project to use your new method, you need to make a few more changes.

Renable Forms Authentication in Web.config

Replace <authentication mode="None" /> with:

<authentication mode="Forms">
  <forms loginUrl="~/Account/LogOn" />
</authentication>

This re-enables forms authentication, and when an unauthorized user tries to access an authentication-required method, redirects them to /Account/LogOn instead of login.aspx.

If it exists, remove this block:

<system.webServer>
    <modules>
      <remove name="FormsAuthentication" />
    </modules>
  </system.webServer>

Call the API in the AccountController

Modify your Login method in AccountController to call the Web API method. Here’s how that looks with RestSharp:

//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{

    var restClient = new RestClient(API base URL, possibly from web.config);
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var request = new RestRequest("Account/Login", Method.POST);

    request.AddObject(model);
    var response = restClient.Execute(request);

    if (response.StatusCode == HttpStatusCode.OK)
    {
        FormsAuthentication.SetAuthCookie(model.Email, model.RememberMe);
        HttpContext.User = new GenericPrincipal(new GenericIdentity(model.Email), null);
        return RedirectToLocal(returnUrl);
    }
    else
    {
        ModelState.AddModelError("", "Invalid login attempt.");
        return View(model);
    }
}

With that, you can log in through your web app. It calls the Web API, gets the cookies back, and stores it so that subsequent calls use the same authentication session.

This also populates the value of Request.IsAuthenticated and HttpContext.Current.User.Identity with your current user.

This code follows existing conventions from the MVC starter code (eg. wrapping the SignInManager in a property, using underscores for member variable names, etc.) so adopt it to whatever style suits you.

About Ashiq Alibhai, PMP

Ashiq has been coding C# since 2005. A desktop, web, and RIA application developer, he's touched ASP.NET MVC, ActiveRecord, Silverlight, NUnit, and all kinds of exciting .NET technologies. He started C# City in order to accelerate his .NET learning.
This entry was posted in Core .NET, Web and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *