Use Cookie Authentication with Web API and HttpClient
Recently I wrote this
article explaining the cookie authentication in ASP.NET Core. A reader asked
whether cookie authentication can be used with ASP.NET Core Web API and that too
when the Web API is being consumed using HttpClient component. This article
explains a possible solution to the problem.
A word of caution
Cookie authentication works great with web applications because everything
runs within a browser. Various pieces of this security scheme such as
authentication cookie and automatic redirection to the login page work great in
the browser. Although you can use cookie authentication with Web API
(because Web API controller is also a controller), doing so is not always
recommended.
Web API is a service and doesn't have any UI elements. So, features such as
redirection URL don't apply to a Web API. Moreover, a Web API can be consumed by
variety of clients including Single Page Applications (SPAs) and non-browser clients. For example, a windows application may utilize a Web API to
get some job done. In such cases the client application may not be able to deal
with the cookie issued as a part of the authentication scheme. Therefore, you
should carefully think whether you want to use cookie authentication with Web
API. There are better alternatives for Web API security such as Json Web Tokens
(JWT) that you can use instead of cookie authentication.
However, if for some reason you want to implement cookie authentication for
Web API you can use the technique illustrated in the remainder of this article.
I am going to use the same ASP.NET Core application that you developed in
this article.
Creating a sample Web API
As an example let's create a Web API that has the following actions :
- Login() : This action will do the task of validating a user's
credentials and will issue the authentication cookie accordingly.
- Logout() : This action will remove the authentication cookie thus
logging the use out of the system.
- Get() : This action is actual Web API action that handles GET verb and
returns data to the caller.
The Login() and Logout() actions will not be auto-mapped to any specific HTTP
verb. That's because your Web API might be need auto-mapping for its main
actions (say, Post() action). The routing of these two actions is configured
differently so that the client can explicitly call them to perform the
respective tasks.
The Get() action is a typical Web API action and maps to GET verb. Although
our sample Web API doesn't include POST, PUT, and DELETE actions you can add
them as per your need. In this example our primary focus will be Login() and
Logout().
Add a new Web API controller - ValuesController - to the project. The Web API
constructor receives the MyAppDbContext object and is shown below :
public class ValuesController : Controller
{
private MyAppDbContext db;
public ValuesController(MyAppDbContext db)
{
this.db = db;
}
}
Notice that the ValuesController doesn't have [Route] attribute added to it.
That's because we configure routing at individual action level as you will see
later.
The Login() action
The Login() action of the ValuesController is shown below :
[Route("api/[controller]/Login")]
[HttpPost]
public IActionResult Login([FromBody]LoginViewModel model)
{
bool isUservalid = false;
MyAppUser user = db.MyAppUsers.Where(usr =>
usr.UserName == model.UserName &&
usr.Password == model.Password).SingleOrDefault();
if (user != null)
{
isUservalid = true;
}
if (isUservalid)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
string[] roles = user.Roles.Split(",");
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.
AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var props = new AuthenticationProperties();
props.IsPersistent = model.RememberMe;
HttpContext.SignInAsync(
CookieAuthenticationDefaults.
AuthenticationScheme,
principal,
props).Wait();
return new ObjectResult("Success");
}
else
{
return new ObjectResult("Error");
}
}
The Login() action is decorated with two attributes - [Route] and [HttpPost].
The [Route] attribute configures the routing such that the Login() method has an
end point URL : api/values/login
The [HttpPost] attribute indicates that Login() should be invoked with a POST
request.
The Login() accepts an object of LoginViewModel. We created this class
earlier while coding the
AccountController. The LoginViewModel contains three properties - UserName,
Password, and RememberMe. The client application needs to sent this object while
attempting to login.
The code then checks whether user credentials are valid. This is done by
fetching a MyAppUser object matching the specified UserName and Password. If
those details are found, the code creates a list of Claim objects,
ClaimsIdentity, ClaimsPrincipal, and AuthenticationScheme. This code should be
quite familiar to you from the previous article and hence I am not going to
discuss it again here.
What's important here is to notice that Login() is issuing the authentication
cookie by calling SignInAsync(). Moreover, it returns a success string "Success"
to the client. If the user can't be validated we don't issue the authentication
cookie and we return "Error" to the caller. This way the client can easily
determine whether the login attempt was successful or not. Of course, you can
also use some HTTP status code based mechanism.
The Logout() action
The Logout() acrtion of ValuesController is shown below :
[Route("api/[controller]/Logout")]
[HttpPost]
public IActionResult Logout()
{
HttpContext.SignOutAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
return new ObjectResult("Success");
}
The [Route] attributed added to the Logout() action configures its end point
URL to /api/values/logout. The [HttpPost] attribute indicates that Logout() will
be invoked using a POST request.
Inside, we simply call SignOutAsync() to remove the authentication cookie
issued in the Login() action.
The Get() action
The Get() action simply returns a string value to the caller and is shown
below :
[Authorize]
[Route("api/[controller]")]
[HttpGet]
public IActionResult Get()
{
string userName = HttpContext.User.Identity.Name;
if (HttpContext.User.IsInRole("Administrator"))
{
return new ObjectResult($"Welcome
{userName} (Administrator)!");
}
else
{
return new ObjectResult($"Welcome
{userName} (Normal User)!");
}
}
Notice the code marked in bold latters. The Get() action is a secured action
and hence we decorate it with [Authorize] attribute. The [Route] and [HttpGet]
attributes map the Get() action to the GET requests.
Inside, we grab the current user's name using the
HttpContext.User.Identity.Name property. We also check whether the user belongs
to Administrator role using HttpContext.User.IsInRole() method. Accordingly we
return a string with a welcome message for that user.
This completes the Web API. Now let's go ahead and consume it using
HttpClient component.
Calling Web API using HttpClient
Now comes the important part - consuming the Web API from a client
application using HttpClient component.
You will need to add two actions to the HomeController - CallApi() and
Logout(). You will also need to add a public constructor as shown below :
private HttpClient client;
public HomeController()
{
HttpClientHandler clientHandler =
new HttpClientHandler();
clientHandler.UseCookies = true;
clientHandler.CookieContainer = new CookieContainer();
client = new HttpClient(clientHandler);
client.BaseAddress = new Uri("http://localhost:49273");
MediaTypeWithQualityHeaderValue contentType =
new MediaTypeWithQualityHeaderValue("application/json");
client.DefaultRequestHeaders.Accept.Add(contentType);
}
The above code creates an instance of HttpClient component and configures it
for calling the Web API.
Notice the code marked in bold letters. It creates a HttpClientHandler object
and sets its UseCookies property is set to true. The value of true
indicates that the HttpClientHandler should use the CookieContainer to store and
send server cookies. The CookieContainer property holds a reference to a new
CookieContainer object.
The HttpClientHandler object is then sent to the HttpClient's constructor.
Properties of HttpClient such as BaseAddress and accept header are configured as
you normally do.
Now that we have configured the HttpClient let's see how the CallApi() action
calls the Web API actions. Have a look at the following code :
public IActionResult CallApi()
{
string loginData = JsonConvert.SerializeObject(
new { UserName = "TestUser",
Password = "TestPassword",
RememberMe = false });
StringContent content = new StringContent(
loginData,System.Text.Encoding.UTF8,"application/json");
HttpResponseMessage loginResponse =
client.PostAsync("/api/values/login", content).Result;
string loginResult = loginResponse.Content.
ReadAsStringAsync().Result;
loginResult = JsonConvert.
DeserializeObject<string>(loginResult);
if (loginResult=="Success")
{
HttpResponseMessage getResponse =
client.GetAsync("/api/values").Result;
ViewData["message"] = JsonConvert.
DeserializeObject<string>
(getResponse.Content.ReadAsStringAsync().Result);
}
else
{
ViewData["message"] = "Error while calling Web API!";
}
return View();
}
Note that our Web API contains Get() action that is secured using the
[Authorize] attribute. So, before you invoke Get() you must log in to the
system.
Therefore, the code first calls the Login() action of the Web API using a
PostAsync() method. The login details such as UserName, Password, and RememberMe
are also sent along with the call as a JSON string.
Recollect that the Login() action of the Web API returns "Success" if the
user credentials are valid. If the login attempt is successful the code goes
ahead and calls the Get() action using the GetAsync() method. The return value
of Get() is stored inside ViewData for the sake of displaying on a view.
What happens if you try to call Get() directly without first calling Login()?
In that case the Get() won't be called and the application will try to redirect
t o the login page. This redirection is a tricky thing in this approach and I
have already highlighted it at the beginning of this article.
A sample successful run of the CallApi() action is shown below :
The markup that goes behind the above view is this :
<h1>@ViewData["message"]</h1>
<form asp-controller="Home"
asp-action="Logout" method="post">
<input type="submit" value="Logout" />
</form>
The Logout button submits the form to the Logout() action of HomeController
which in turn calls the Logout() action of the Web API :
[HttpPost]
public IActionResult Logout()
{
HttpResponseMessage logoutResponse =
client.PostAsync("/api/values/logout", null).Result;
string logoutResult = logoutResponse.Content.
ReadAsStringAsync().Result;
logoutResult = JsonConvert.
DeserializeObject<string>(logoutResult);
ViewData["message"] = logoutResult;
return View();
}
The Logout view simply displays the value of message from ViewData and hence
is not discussed here.
Ok. Set a breakpoint at the CallApi() action. Run the application, enter
/home/callapi in the addressbar and see how the code works.
That's it for now! Keep coding !!