Request from desktop client hits the wrong controller action - asp.net

From a Windows Forms desktop application, I make a request to an end-point in my ASP.NET MVC Web Application (not Web API).
The request is made for the endpoint "~/Account/APILogin" but it hits the "~/Account/Login" (GET) action even when I am making a post request to the former. Why is that so?
Here are the relevant bits of code:
In the ASP.NET MVC Application
class AccountController : Controller
{
// The request comes to this guy
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
// get request for Web browser clients
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
// post request for Web browser clients
}
// It should really have hit this
[HttpPost]
public JsonResult APILogin(LoginRequest loginRequest)
{
if (loginRequest == null || string.IsNullOrEmpty(loginRequest.UserName) || string.IsNullOrEmpty(loginRequest.Password))
{
return Json(LoginResult.CreateFailure("Invalid login. Please try again."));
}
var hashedPassword = UserManager.PasswordHasher.HashPassword(loginRequest.Password);
var user = BusinessManager.GetUser(loginRequest.UserName, hashedPassword);
if (user == null)
{
return Json(LoginResult.CreateFailure("Invalid login. Please try again."));
}
return Json(LoginResult.CreateSuccess());
}
}
In the Global.asax of the ASP.NET MVC Web Application
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
}
}
In the Windows Forms desktop client application
private async void btnOk_Click(object sender, EventArgs e)
{
var client = new Client();
var loginRequest = new LoginRequest { UserName = txtUserName.Text.Trim(), Password = txtPassword.Text.Trim() };
var loginResult = await client.LoginAsync(loginRequest);
if (loginResult.Succeeded)
{
Close();
}
else
{
ReportInvalidLogin(loginResult.FailureMessage);
}
}
In a class library that makes the HTTP Request on behalf of clients to the ASP.NET Web Web Application
class Client : IDisposable
{
private WebClient _webClient = null;
private string _baseUrl = null;
public Client()
{
_webClient = new WebClient();
_baseUrl = ConfigurationManager.AppSettings["WebApplicationBasePath"];
if (string.IsNullOrEmpty(_baseUrl))
{
throw new ArgumentNullException("Please add a key named WebApplicationBasePath to the configuration file with the base Url of the server web application.");
}
}
public async Task<LoginResult> LoginAsync(LoginRequest loginRequest)
{
var loginUrl = string.Format($"{_baseUrl}Account/APILogin");
var data = await Task.Factory.StartNew<string>(() => JsonConvert.SerializeObject(loginRequest));
_webClient.Headers.Add(HttpRequestHeader.ContentType, "application/json");
var responseString = await _webClient.UploadStringTaskAsync(loginUrl, "POST", data);
var loginResult = await Task.Factory.StartNew<LoginResult>(() =>
JsonConvert.DeserializeObject<LoginResult>(responseString));
return loginResult;
}
}
I do not have an AuthorizeAttribute applied yet, so that, too, as a possible cause is out of the question.

Somewhere in your stack (possibly IIS settings, web.config, or applying a global filter) authentication is being required for all requests, unless explicitly marked with the [AllowAnonymous] attribute. The solution is simple, add that attribute:
[HttpPost]
[AllowAnonymous]
public JsonResult APILogin(LoginRequest loginRequest)

Related

How to check something before execute http request aspnet core web api 6

My customer want to validate token from query param like this
http://localhost/api/v1/users?token=xxxx
I can do like this:
[CustomAuthorize(Authorities = new[] { Constants.RoleGuest })]
[HTTPGet]
public async Task<IActionResult> Get(string token){
//call validate token function with token provided
//do something
}
Is there a way to implement automatic token authentication that does this for all requests except login and register?
It sucks to have to call the authentication function on every http request. Is this implementable as a custom attribute ?
This question don't mention how to implement authen and authorization. Main popurse is check something when user request to any endpoint. In this situation, it is token. It isn't same access token and refresh token
Thanks for all the help!
You can use action filter and custom attribute to implement it.
public class MyAuth : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var actionInfo = context.ActionDescriptor as ControllerActionDescriptor;
var token = context.HttpContext.Request.Query.ContainsKey("token")
? Convert.ToString(context.HttpContext.Request.Query["token"])
: string.Empty;
var shouldStop = !IsValidToken(token, actionInfo);
if (shouldStop)
{
context.Result = new UnauthorizedResult();
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
private bool IsValidToken(string token, ControllerActionDescriptor actionInfo)
{
var valid = false;
var controllerName = actionInfo?.ControllerName;
var actionName = actionInfo?.ActionName;
var roles =
(actionInfo?.MethodInfo.GetCustomAttributes(typeof(CustomAuthorize), true)?.FirstOrDefault() as
CustomAuthorize).Roles;
// your token validation logic goes here
return valid;
}
}
public class CustomAuthorize : Attribute
{
public string[] Roles { get; }
public CustomAuthorize(string[] roles)
{
Roles = roles;
}
}
And in the program.cs you can register the Action filter as below
builder.Services.AddControllers(_ =>
{
_.Filters.Add(typeof(MyAuth));
});
Finally, your action method would look like below -
[CustomAuthorize(new string[]{Constants.RoleGuest})]
[HTTPGet]
public async Task<IActionResult> Get(){
// do actual work.
// this method will be only called if your validation logic pass.
}

Validate 3rd Party Cookies with ASP.NET Core Web Api

I am using ORY Kratos for identity and my frontend SPA (React App) is authenticating against the Kratos Login Server and gets a session cookie back.
Now I want to secure my ASP.NET Core Web Api in a way, that a user can only call certain methods protected with the [Authorize] attribute when attaching a valid cookie to the request. For this, I need to validate the cookie from every incoming request. So I am looking for a way to configure Authentication and add custom logic to validate the cookie (I need to make an API call to Kratos to validate it).
The cookie I want to validate has not been issued by the ASP.NET Core App that wants to validate it.
All the samples I found so far, are also issuing the cookie on the same server but I need to validate an external one.
This is what my cookie looks like:
In the Dev Tools, I can validate that the Cookie is attached to the requests header:
This is, what I've tried so far:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "ory_kratos_session";
options.Cookie.Path = "/";
options.Cookie.Domain = "localhost";
options.Cookie.HttpOnly = true;
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
services.AddScoped<CustomCookieAuthenticationEvents>();
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
}
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public CustomCookieAuthenticationEvents() {}
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// Never gets called
}
}
Logs:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[7]
Cookies was not authenticated. Failure message: Unprotect ticket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
AuthenticationScheme: Cookies was challenged.
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HM6IBAO4PLLL" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET https://localhost:5001/weatherforecast - - - 302 0 - 75.3183ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET https://localhost:5001/Account/Login?ReturnUrl=%2Fweatherforecast - -
According to the cookie authentication handler's source codes, I found it will read the cookie before goes to the CustomCookieAuthenticationEvents .
Some part of codes as below:
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name!);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
// Only store _sessionKey if it matches an existing session. Otherwise we'll create a new one.
ticket = await Options.SessionStore.RetrieveAsync(claim.Value);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
_sessionKey = claim.Value;
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey!);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
// Finally we have a valid ticket
return AuthenticateResult.Success(ticket);
}
If you still want to use cookie authentication, you need to rewrite the handler. So I suggest you could write a custom AuthenticationHandler and AuthenticationSchemeOptions class like below and register the class in the startup.cs directly. Then you could use [Authorize(AuthenticationSchemes = "Test")] to set the special AuthenticationSchemes.
Codes:
public class ValidateHashAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
public class ValidateHashAuthenticationHandler
: AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
{
public ValidateHashAuthenticationHandler(
IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
//TokenModel model;
// validation comes in here
if (!Request.Headers.ContainsKey("X-Base-Token"))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
var token = Request.Headers["X-Base-Token"].ToString();
try
{
// convert the input string into byte stream
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
{
// deserialize stream into token model object
//model = Serializer.Deserialize<TokenModel>(stream);
}
}
catch (System.Exception ex)
{
Console.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
//if (model != null)
//{
// // success case AuthenticationTicket generation
// // happens from here
// // create claims array from the model
// var claims = new[] {
// new Claim(ClaimTypes.NameIdentifier, model.UserId.ToString()),
// new Claim(ClaimTypes.Email, model.EmailAddress),
// new Claim(ClaimTypes.Name, model.Name) };
// // generate claimsIdentity on the name of the class
// var claimsIdentity = new ClaimsIdentity(claims,
// nameof(ValidateHashAuthenticationHandler));
// // generate AuthenticationTicket from the Identity
// // and current authentication scheme
// var ticket = new AuthenticationTicket(
// new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// // pass on the ticket to the middleware
// return Task.FromResult(AuthenticateResult.Success(ticket));
//}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class TokenModel
{
public int UserId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
}
Startup.cs add below codes into the ConfigureServices method:
services.AddAuthentication(options =>
{
options.DefaultScheme
= "Test";
})
.AddScheme<ValidateHashAuthenticationSchemeOptions, ValidateHashAuthenticationHandler>
("Test", null);
Usage:
Controller:
[Authorize(AuthenticationSchemes = "Test")]
So I now solved it for myself differently by creating a custom Authentication Handler, which takes care of checking the Cookie that gets send to the Web API.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSingleton(new KratosService("http://localhost:4433"));
services
.AddAuthentication("Kratos")
.AddScheme<KratosAuthenticationOptions, KratosAuthenticationHandler>("Kratos", null);
// ...
}
If you are interested in the implementation that did the job for me, I have attached the additional files below. It is also worth mentioning, that Kratos supports two way of Authenticating: Cookies and Bearer Tokens, depending on if you talk to it via Web App or an API. My implementation supports both. You can find a working Sample with ASP.NET Core and React here: https://github.com/robinmanuelthiel/kratos-demo
KratosAuthenticationHandler.cs:
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace KratosDemo.Server.Kratos
{
public class KratosAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class KratosAuthenticationHandler : AuthenticationHandler<KratosAuthenticationOptions>
{
readonly KratosService _kratosService;
readonly string _sessionCookieName = "ory_kratos_session";
public KratosAuthenticationHandler(
IOptionsMonitor<KratosAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
KratosService kratosService
)
: base(options, logger, encoder, clock)
{
_kratosService = kratosService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ORY Kratos can authenticate against an API through two different methods:
// Cookie Authentication is for Browser Clients and sends a Session Cookie with each request.
// Bearer Token Authentication is for Native Apps and other APIs and sends an Authentication header with each request.
// We are validating both ways here by sending a /whoami request to ORY Kratos passing the provided authentication
// methods on to Kratos.
try
{
// Check, if Cookie was set
if (Request.Cookies.ContainsKey(_sessionCookieName))
{
var cookie = Request.Cookies[_sessionCookieName];
var id = await _kratosService.GetUserIdByCookie(_sessionCookieName, cookie);
return ValidateToken(id);
}
// Check, if Authorization header was set
if (Request.Headers.ContainsKey("Authorization"))
{
var token = Request.Headers["Authorization"];
var id = await _kratosService.GetUserIdByToken(token);
return ValidateToken(id);
}
// If neither Cookie nor Authorization header was set, the request can't be authenticated.
return AuthenticateResult.NoResult();
}
catch (Exception ex)
{
// If an error occurs while trying to validate the token, the Authentication request fails.
return AuthenticateResult.Fail(ex.Message);
}
}
private AuthenticateResult ValidateToken(string userId)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new System.Security.Principal.GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
KratosService.cs:
using System.Threading.Tasks;
using System.Net.Http;
using System.Text.Json;
using System;
namespace KratosDemo.Server.Kratos
{
public class KratosService
{
private readonly string _kratosUrl;
private readonly HttpClient _client;
public KratosService(string kratosUrl)
{
_client = new HttpClient();
_kratosUrl = kratosUrl;
}
public async Task<string> GetUserIdByToken(string token)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Authorization", token);
return await SendWhoamiRequestAsync(request);
}
public async Task<string> GetUserIdByCookie(string cookieName, string cookieContent)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Cookie", $"{cookieName}={cookieContent}");
return await SendWhoamiRequestAsync(request);
}
private async Task<string> SendWhoamiRequestAsync(HttpRequestMessage request)
{
var res = await _client.SendAsync(request);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();
var whoami = JsonSerializer.Deserialize<Whoami>(json);
if (!whoami.Active)
throw new InvalidOperationException("Session is not active.");
return whoami.Identity.Id;
}
}
}
Whoami.cs:
using System;
using System.Text.Json.Serialization;
namespace KratosDemo.Server.Kratos
{
public class Whoami
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("active")]
public bool Active { get; set; }
[JsonPropertyName("expires_at")]
public DateTime ExpiresAt { get; set; }
[JsonPropertyName("authenticated_at")]
public DateTime AuthenticatedAt { get; set; }
[JsonPropertyName("issued_at")]
public DateTime IssuedAt { get; set; }
[JsonPropertyName("identity")]
public Identity Identity { get; set; }
}
public class Identity
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
}

.NET Core API + MVC Core Client supported by RestSharp

I have created .NET Core Web Api with JWT authentication. Now, I am in the middle of creating web app using MVC Core. In MVC project I have API client wrapper:
Interface:
public interface IWebApiService
{
Task<T> AuthenticateAsync<T>(string userName);
Task<T> GetAsync<T>(string action, string authToken);
Task PutAsync<T>(string action, T data, string authToken);
Task PostAsync<T>(string action, T data, string authToken);
}
Implementation:
public class WebApiService : IWebApiService
{
private readonly WebApiSettings _webApiSettings;
public WebApiService(WebApiSettings webApiSettings)
{
_webApiSettings = webApiSettings;
}
public async Task<T> AuthenticateAsync<T>(string userName)
{
var client = new RestClient(_webApiSettings.BaseUri);
var request = new RestRequest("/Login", Method.POST)
{
RequestFormat = DataFormat.Json
};
request.AddBody(new { UserName = userName });
var response = await client.ExecuteTaskAsync(request);
if (response.IsSuccessful)
{
return JsonConvert.DeserializeObject<T>(response.Content);
}
throw new ApiException(response.StatusCode.ToString(), response.ErrorMessage);
}
public async Task<T> GetAsync<T>(string action, string authToken)
{
var client = new RestClient(_webApiSettings.BaseUri);
var request = new RestRequest(action, Method.GET)
{
RequestFormat = DataFormat.Json
};
request.AddHeader("Authorization", $"Bearer {authToken}");
var response = await client.ExecuteTaskAsync(request);
if (response.IsSuccessful)
{
return JsonConvert.DeserializeObject<T>(response.Content);
}
throw new ApiException(response.StatusCode.ToString(), response.ErrorMessage);
}
public Task PutAsync<T>(string action, T data, string authToken)
{
// TODO
throw new NotImplementedException();
}
public Task PostAsync<T>(string action, T data, string authToken)
{
// TODO
throw new NotImplementedException();
}
}
MVC Login Controller:
public class LoginController : Controller
{
private readonly IWebApiService _webApiService;
public LoginController(IWebApiService webApiService)
{
_webApiService = webApiService;
}
public async Task<IActionResult> Get(string redirectUrl)
{
var user = User.Identity.Name;
if(user == null)
throw new WebInterfaceException("Invalid username.");
var response = await _webApiService.AuthenticateAsync<JwtToken>(user);
HttpContext.Session.SetObjectAsJson("Token", response);
return Redirect(redirectUrl ?? "/Home/Index");
}
}
I keep JWT object in session as I didn't find better solution for storing tokens in MVC Core.
Below example controller:
public class ExampleController : Controller
{
private readonly IWebApiService _webApiService;
public ExampleController(IWebApiService webApiService)
{
_webApiService = webApiService;
}
[HttpGet]
public async Task<IActionResult> Browse()
{
var jwtToken = HttpContext.Session.GetObjectFromJson<JwtToken>("Token");
if (jwtToken == null)
{
return RedirectToAction("Get", "Login", new { redirectUrl = Request.Path});
}
var response = await _webApiService.GetAsync<IEnumerable<ExampleBrowseViewModel>>("/Examples", jwtToken.Token);
return Json(response);
}
}
My problem is that in every controller action I will have to check if token is not null. If it's null, I am redirecting to Login page where I am retrieving token from API and redirecting to originally requested page. I would like to have some token handler where so I will not repeat the same code over and over. Additionally in my JWT object I have token expiration time and I would like to refresh it once it will expire so user could continue sending requests to API.
Can you give me few advises so I could accomplish this?

Too many redirects - Owin External Login with Facebook Asp.Net

I'm creating a web-api where I need to log people in using Facebook.
I'm following this guide.
Once I provide my credentials to Facebook, it should redirect to an Action but instead it says: "Too many redirects."
This is what I've got in my Startup.cs:
app.UseExternalSignInCookie(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalCookie);
FacebookAuthenticationOptions facebookAuthOptions = new FacebookAuthenticationOptions()
{
AppId = "myAppId",
AppSecret = "myAppKey",
Provider = new FacebookAuthProvider()
};
app.UseFacebookAuthentication(facebookAuthOptions);
This is my FacebookAuthProvider: class:
public class FacebookAuthProvider : FacebookAuthenticationProvider
{
public override Task Authenticated(FacebookAuthenticatedContext context)
{
context.Identity.AddClaim(new System.Security.Claims.Claim("ExternalAccessToken", context.AccessToken));
return Task.FromResult<object>(null);
}
}
This is my ChallengeResult class:
public class ChallengeResult : IHttpActionResult
{
public string LoginProvider { get; set; }
public HttpRequestMessage Request { get; set; }
public ChallengeResult(string loginProvider, ApiController controller)
{
LoginProvider = loginProvider;
Request = controller.Request;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
Request.GetOwinContext().Authentication.Challenge(LoginProvider);
HttpResponseMessage response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
return Task.FromResult<HttpResponseMessage>(response);
}
}
And this is the controller that I'm using to get the token from Facebook after user has logged in:
[HttpGet]
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
//[Route("ExternalLogin", Name = "ExternalLogin")]
public IHttpActionResult GetExternalLogin(string provider)
{
string redirectUri = string.Empty;
AppUserManager manager = new AppUserManager(new AppUserStore(new AppContext()));
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
UserLoginInfo loginInfo = new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey);
IdentityUser user = manager.Find(loginInfo);
bool hasRegistered = user != null;
ValidateRedirectUri(this.Request, ref redirectUri);
redirectUri = String.Format("{0}#external_access_token={1}&provider={2}&haslocalaccount={3}&external_user_name={4}",
redirectUri,
externalLogin.AccessToken,
externalLogin.LoginProvider,
hasRegistered.ToString(),
externalLogin.UserName);
return Redirect(redirectUri);
}
One thing that I'm really curious about, is that, if I uncomment this line:
[Route("ExternalLogin", Name = "ExternalLogin")]
And try to access that controller with that new route, it says that User (The one in the GetExternalLogin's if) is null.
This is the link that I'm using to test:
http://localhost:62887/api/ExternalAuth/GetExternalLogin?provider=Facebook&redirect_uri=http://localhost:62887/api/ExternalAuth/LoggedIn
And after the user has successfully logged in, this is the action that he's supposed to be redirected:
[HttpGet]
public IHttpActionResult LoggedIn()
{
return Ok(new { Message = "You've been successfully logged in! :)" });
}
I'm gonna kill myself, I finally got it working. I just had to update the NuGet Package from 2.1 to 3.1... >:/

MVC Application using WebApi, Identity, and using Cookies safely

I had the requirement to have my Identity-enabled MVC site with WebApi use cookies for authentication for both the site and web api. Anyone who has dealt with this probably knows that this could be vulnerable to XSS attacks as the regular login cookie could be sent to your webapi methods by visiting a malicious page.
The strange requirement to use cookies with web api is the root of the matter. Is there any way to do this safely?
I have a solution using Forms Authentication in an AuthorizationFilter (posted below) but I was hoping to leverage the features of the Identity framework such as claims and sign-out everywhere.
using System;
using System.Web.Http.Filters;
using System.Web.Http;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;
namespace Filters
{
/// <summary>
/// An authentication filter that uses forms authentication cookies.
/// </summary>
/// <remarks>Use the *Cookie static methods to manipulate the cookie on the client</remarks>
public class FormsAuthenticationFilter : Attribute, IAuthenticationFilter
{
public static long Timeout { get; set; }
public static string CookieName { get; set; }
public FormsAuthenticationFilter()
{
// Default Values
FormsAuthenticationFilter.Timeout = FormsAuthentication.Timeout.Minutes;
FormsAuthenticationFilter.CookieName = "WebApi";
}
public FormsAuthenticationFilter(long Timeout, string CookieName)
{
FormsAuthenticationFilter.Timeout = Timeout;
FormsAuthenticationFilter.CookieName = CookieName;
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
// Get cookie
HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthenticationFilter.CookieName];
//If no cookie then do nothing
if (cookie == null)
{
return Task.FromResult(0);
}
//If empty cookie then raise error
if (String.IsNullOrEmpty(cookie.Value))
{
context.ErrorResult = new AuthenticationFailureResult("Empty ticket", request);
return Task.FromResult(0);
}
//Decrypt ticket
FormsAuthenticationTicket authTicket = default(FormsAuthenticationTicket);
try
{
authTicket = FormsAuthentication.Decrypt(cookie.Value);
}
catch (Exception)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid ticket", request);
return Task.FromResult(0);
}
//Check if expired
if (authTicket.Expired)
{
context.ErrorResult = new AuthenticationFailureResult("Ticket expired", request);
return Task.FromResult(0);
}
//If caching roles in userData field then extract
string[] roles = authTicket.UserData.Split(new char[] { '|' });
// Create the IIdentity instance
IIdentity id = new FormsIdentity(authTicket);
// Create the IPrinciple instance
IPrincipal principal = new GenericPrincipal(id, roles);
// Set the context user
context.Principal = principal;
// Update ticket if needed (sliding window expiration)
if ((authTicket.Expiration - DateTime.Now).TotalMinutes < (FormsAuthenticationFilter.Timeout / 2))
{
RenewCookie(authTicket);
}
return Task.FromResult(0);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
//Do nothing
return Task.FromResult(0);
}
public bool AllowMultiple
{
get { return false; }
}
/// <summary>
/// Renews the cookie on the client using the specified FormsAuthenticationTicket
/// </summary>
/// <param name="OldTicket">A still-valid but aging FormsAuthenticationTicket that should be renewed</param>
/// <remarks></remarks>
protected static void RenewCookie(FormsAuthenticationTicket OldTicket)
{
HttpContext.Current.Response.Cookies.Add(GetCookie(OldTicket.Name, OldTicket.UserData));
}
/// <summary>
/// Sets the authentication cookie on the client
/// </summary>
/// <param name="UserName">The username to set the cookie for</param>
/// <remarks></remarks>
public static void SetCookie(String user, IList<string> roles)
{
HttpContext.Current.Response.Cookies.Add(GetCookie(user, string.Join("|", roles)));
}
/// <summary>
/// Removes the authentication cookie on the client
/// </summary>
/// <remarks>Cookie is removed by setting the expires property to in the past, may not work on all clients</remarks>
public static void RemoveCookie()
{
if ((HttpContext.Current.Response.Cookies[FormsAuthenticationFilter.CookieName] != null))
{
HttpContext.Current.Response.Cookies[FormsAuthenticationFilter.CookieName].Expires = DateTime.Now.AddDays(-1);
}
}
private static HttpCookie GetCookie(string UserName, string UserData)
{
//Create forms auth ticket
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, UserName, DateTime.Now, DateTime.Now.AddMinutes(FormsAuthenticationFilter.Timeout), false, UserData);
//Create cookie with encrypted contents
HttpCookie cookie = new HttpCookie(FormsAuthenticationFilter.CookieName, FormsAuthentication.Encrypt(ticket));
cookie.Expires = DateTime.Now.AddMinutes(FormsAuthenticationFilter.Timeout);
//Return it
return cookie;
}
protected class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
this.ReasonPhrase = reasonPhrase;
this.Request = request;
}
public string ReasonPhrase { get; set; }
public HttpRequestMessage Request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
}
}
Let me preface this answer by saying this is only a solution if WebApi methods are never called via javascript from your website. This should only be used if you plan to use WebApi from other clients such as a mobile app.
The solution lies in using a separate cookie for WebApi. WebApi will reject the normal site authorization cookie and only use its own.
In your WebApiConfig class add a constant and a static method to configure the cookie middle-ware:
public const string WebApiCookieAuthenticationType = "WebApiCookie";
public static CookieAuthenticationOptions CreateCookieAuthenticationOptions()
{
return new CookieAuthenticationOptions
{
AuthenticationType = WebApiConfig.WebApiCookieAuthenticationType,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
CookieName = ".AspNet.WebApiCookie",
CookiePath = "/api",
ExpireTimeSpan = TimeSpan.FromDays(14),
SlidingExpiration = true,
LoginPath = new PathString("/api/auth"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(60),
regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, WebApiConfig.WebApiCookieAuthenticationType)
),
OnApplyRedirect = ctx => {}
}
};
}
Note the AuthenticationType and AuthenticationMode, these will prevent it from interfering with normal site cookies after we register it. We do this next in your Startup class placing it just below the existing app.UseCookieAuthentication() call:
// Setup WebApi Cookie Authentication
app.UseCookieAuthentication(WebApiConfig.CreateCookieAuthenticationOptions());
We also need to configure WebApi to ignore the normal site cookies and process our special cookies as well. Do this in your WebApiConfig.Register() method:
// Ignore website auth
config.SuppressHostPrincipal();
config.Filters.Add(new HostAuthenticationFilter(WebApiCookieAuthenticationType));
Now we are setup but how do you issue the auth cookie? Create an authorization controller method in your webapi that uses the standard Identity SignInManager but with an extra step:
[AllowAnonymous]
public async Task<IHttpActionResult> Authenticate(string userName, string password)
{
// Get signIn manager
var signInManager = HttpContext.Current.Request.GetOwinContext().Get<ApplicationSignInManager>();
if (signInManager == null)
{
return BadRequest();
}
// Important!
signInManager.AuthenticationType = WebApiConfig.WebApiCookieAuthenticationType;
// Sign In
var result = await signInManager.PasswordSignInAsync(userName, password, false, shouldLockout: true);
if (result == SignInStatus.Success)
{
// Return
return Ok();
}
// Failure
return BadRequest();
}
Pay particular attention to where we set the SignInManager's AuthenticationType, this will make it use our special WebApi cookie instead of the standard site cookie.
Gotchas
One gotcha is that the default site template overrides the SignInManager's CreateUserIdentityAsync method with a hard-coded reference to DefaultAuthenticationTypes.ApplicationCookie authentication type. This will interfere with this solution. To fix it you can change the override to this:
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
if (this.AuthenticationType == DefaultAuthenticationTypes.ApplicationCookie)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
else
{
return base.CreateUserIdentityAsync(user);
}
}
Another gotcha is compression. If you are using the great Microsoft.AspNet.WebApi.Extensions.Compression package you will need to disable compression for web api methods using the SignInManager to log in/out as it writes the response before Identity can add the Set-Cookie header to the response.

Resources