How to protect a Web API using ASP.NET 5 MVC 6

8k views Asked by At

I have a nice ASP.NET 5 / MVC 6 app up and running. Essentially for this purpose it is just the normal sample app you get when you start a new project to keep it simple. So far I can:

  • Register a user
  • Login
  • Logout
  • Protect a page (forcing login etc)

Now, what I would like is to provide an API mechanism for a app to login and get an authentication token. Specifically I am working on two mobile apps to test with, one using Angular / Cordova and one using Xamarin.

I have looked high and low and I cannot seem to find an example yet that shows how to make this work. Every example I find so far assumes the user will login via the normal web form / post cycle and then be taken to a page that loads Angular and this the authentication token is already in the browser.

The relevant code from the AccountController.cs file for the MVC controller is below. What I ultimately want is the equivalent functionality but from a pure API call that allows Angular / Xamarin to send it a username / password and get back a authentication token or failure.

    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
    {
        ViewBag.ReturnUrl = returnUrl;
        if (ModelState.IsValid)
        {
            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, set shouldLockout: true
            var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
            if (result.Succeeded)
            {
                return RedirectToLocal(returnUrl);
            }
            if (result.RequiresTwoFactor)
            {
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            }
            if (result.IsLockedOut)
            {
                return View("Lockout");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                return View(model);
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);
    }

What is the recommended way to protect a Web API using ASP.NET MVC 6?

2

There are 2 answers

5
Frank Fajardo On

I think the recommended approach for securing WebApi2 is through an Authorisation Server. The Authorisation Server takes care of generating tokens. But based on this, the OAuth2 based authorization server that is part of Katana3 has been dropped from Asp.Net 5.

I assume your app is not a live app yet, since both ASP.NET 5 and MVC 6 are not in their final release stage yet. So if you are okay to change your identity/authentication process, you could use Thinktecture's IdentityServer3.

Dominick Baier has blogged about the The State of Security on ASP.NET 5 and MVC 6 and where IdSvr3 comes into the picture. That blog has a link to a Github repository for sample API Controller, and also a API client. It also has sample for an MVC Web app. And it can work with Asp.Net Identity.

UPDATE:

If that does not work for you, you could try AspNet.Security.OpenIdConnect.Server. Note that it has open issues on Github, so you may encounter problems using it. Note also its dependencies, particularly the azureadwebstacknightly.

Please note that ASP.NET 5 and MVC 6 may be in a stable beta release, but they are still in beta release. It could still change.

Also, IdSvr3 v2.0 may be in its final release, but it is developed by a separate team. And it is only been released 2 weeks ago, so IMHO like most software, you can encounter things that may likely missed their tests. Note the ASP.NET Team, last week, tweeted about IdSvr3's (v2.0) release, so it appears they are endorsing it.

4
Kunal B. On

Here is the blog-post that I had promised, this is the first draft, and would be helpful for someone who has a some experience with ASP.NET MVC 5: Bookstore - Web API with Authorization

Complete Source at Github: https://github.com/kbajpai/bookstore

The API with Authorization:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using Bookstore.Models;

namespace Bookstore.Controllers
{
  public class BooksController : ApiController
  {
    private BooksDbContext db = new BooksDbContext();

    // GET: api/Books
    [Authorize(Roles="superuser,user")]
    public IQueryable<Book> GetBooks()
    {
      return db.Books;
    }

    // GET: api/Books/5
    [ResponseType(typeof(Book))]
    [Authorize(Roles = "superuser,user")]
    public async Task<IHttpActionResult> GetBook(string id)
    {
      Book book = await db.Books.FindAsync(id);
      if (book == null)
      {
        return NotFound();
      }

      return Ok(book);
    }

    // PUT: api/Books/5
    [ResponseType(typeof(void))]
    [Authorize(Roles = "superuser")]
    public async Task<IHttpActionResult> PutBook(string id, Book book)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }

      if (id != book.Id)
      {
        return BadRequest();
      }

      db.Entry(book).State = EntityState.Modified;

      try
      {
        await db.SaveChangesAsync();
      }
      catch (DbUpdateConcurrencyException)
      {
        if (!BookExists(id))
        {
          return NotFound();
        }
        else
        {
          throw;
        }
      }

      return StatusCode(HttpStatusCode.NoContent);
    }

    // POST: api/Books
    [Authorize(Roles = "superuser")]
    [ResponseType(typeof(Book))]
    public async Task<IHttpActionResult> PostBook(Book book)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }

      db.Books.Add(book);

      try
      {
        await db.SaveChangesAsync();
      }
      catch (DbUpdateException)
      {
        if (BookExists(book.Id))
        {
          return Conflict();
        }
        else
        {
          throw;
        }
      }

      return CreatedAtRoute("DefaultApi", new { id = book.Id }, book);
    }

    // DELETE: api/Books/5
    [Authorize(Roles = "superuser")]
    [ResponseType(typeof(Book))]
    public async Task<IHttpActionResult> DeleteBook(string id)
    {
      Book book = await db.Books.FindAsync(id);
      if (book == null)
      {
        return NotFound();
      }

      db.Books.Remove(book);
      await db.SaveChangesAsync();

      return Ok(book);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        db.Dispose();
      }
      base.Dispose(disposing);
    }

    private bool BookExists(string id)
    {
      return db.Books.Count(e => e.Id == id) > 0;
    }
  }
}