'ASP.NET Core MVC cookie strange behaviour
My project is built on microservices. The ManagmentUsersApplication microservice contains the AuthenticateUserMiddleware, which checks the user for authorization through jwt tokens, the AuthApplication microservice is responsible for authorization. I am doing cookie authentication by passing refresh and bearer tokens. The AuthenticateUserMiddleware class is responsible for the authentication itself and the user's access to the microservice he needs (the link to it is passed to the AccountController in the AuthApplication). At the end of the AccountController Login method, the access cookies are successfully added to the response, but when trying to read them in InvokeAsync AuthenticateUserMiddleware I found that the cookies are empty. Class code:
AuthenticateUserMiddleware:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using ManagmentUsersApplication.Configuration;
using ManagmentUsersApplication.Helpers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
namespace ManagmentUsersApplication.Middlewares
{
public class AuthenticateUserMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly TokenValidationParameters _tokenValidationParameters;
private bool bearerInvalid = false;
private bool refreshIvalid = false;
public AuthenticateUserMiddleware(RequestDelegate next,
IHttpClientFactory httpClientFactory,
TokenValidationParameters tokenValidationParameters,
ILogger logger)
{
_next = next;
_httpClientFactory = httpClientFactory;
_tokenValidationParameters = tokenValidationParameters;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
refreshIvalid = false;
bearerInvalid = false;
List<string> ignorePaths = new List<string>()
{
new string("/health-check"),
new string("/js"),
new string("/_content"),
new string("/_blazor"),
new string("/robots.txt"),
new string("/assets"),
new string("/_framework"),
new string("/css")
};
if (ignorePaths.Any(x => context.Request.Path.ToString().StartsWith(x)))
{
await _next.Invoke(context);
return;
}
var s = AppConfiguration.ApplicationDomain;
string bearerToken = null;
if (context.Request.Cookies.TryGetValue("BearerToken", out string _bearerToken)) bearerToken = _bearerToken;
var currentScheme = "https";
if (context.Request.Headers.TryGetValue("x-forwarded-proto", out StringValues _scheme))
{
currentScheme = _scheme;
}
var jwtTokenHandler = new JwtSecurityTokenHandler();
try
{
var tokenVerification = jwtTokenHandler.ValidateToken(bearerToken, _tokenValidationParameters, out var validatedToken);
context.User = tokenVerification;
var utcExpiryDate = long.Parse(tokenVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp).Value);
var expiryDate = SecurityHelper.UnixTimeStampToDateTime(utcExpiryDate);
if (expiryDate > DateTime.UtcNow)
{
await _next.Invoke(context); return;
}
if (expiryDate <= DateTime.UtcNow) { bearerInvalid = true; }
}
catch (SecurityTokenExpiredException ex) { bearerInvalid = true; }
catch (Exception ex) { bearerInvalid = true; }
if (bearerInvalid)
{
try
{
var client = _httpClientFactory.CreateClient("UserService");
if (!context.Request.Cookies.TryGetValue("RefreshToken", out string refreshToken)) throw new ArgumentException("Refresh token not found");
var model = await client.GetFromJsonAsync<RefreshUserTokenResponse>($"Authenticate/RefreshToken?refreshToken={refreshToken}");
if (model.Result != RefreshUserTokenResult.Success)
{
throw new ArgumentException("RefreshToken invalid");
}
context.Response.Cookies.Append("BearerToken", model.BearerToken, new CookieOptions { Domain = AppConfiguration.ApplicationDomain });
context.Response.Cookies.Append("RefreshToken", model.RefreshToken, new CookieOptions { Expires = DateTime.UtcNow.AddMonths(6), Domain = AppConfiguration.ApplicationDomain });
try
{
var tokenVerification = jwtTokenHandler.ValidateToken(model.BearerToken, _tokenValidationParameters, out var validatedToken);
context.User = tokenVerification;
}
catch (Exception ex)
{
await _logger.LogErrorAsync(ex);
}
}
catch (ArgumentException ex) { refreshIvalid = true; }
catch (Exception ex) { refreshIvalid = true; }
}
if (refreshIvalid)
{
var redirectUri = $"{currentScheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.Response.Cookies.Delete("BearerToken", new CookieOptions { Domain = AppConfiguration.ApplicationDomain });
context.Response.Redirect($"{AppConfiguration.AuthApplicationUrl}Account/Login?redirectUri={redirectUri}");
}
else await _next.Invoke(context);
}
}
public enum RefreshUserTokenResult
{
Success,
InvalidRefreshToken,
ServerError
}
public class RefreshUserTokenResponse
{
public string BearerToken { get; set; }
public string RefreshToken { get; set; }
public RefreshUserTokenResult Result { get; set; }
}
}
AccountController:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using AuthApplication.Configuration;
using AuthApplication.Handlers;
using AuthApplication.Models;
using AuthApplication.Models.Account;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace AuthApplication.Controllers
{
public class AccountController : Controller
{
private readonly IAuthenticateUserHandler _authenticateUserHandler;
private readonly ILogoutUserHandler _logoutUserHandler;
public AccountController(IAuthenticateUserHandler authenticateUserHandler, ILogoutUserHandler logoutUserHandler)
{
_authenticateUserHandler = authenticateUserHandler;
_logoutUserHandler = logoutUserHandler;
}
[HttpGet]
public async Task<IActionResult> Login()
{
var viewModel = new LoginAccountViewModel();
return View(viewModel);
}
[HttpGet]
public async Task<IActionResult> Logout([FromQuery] string redirectUri)
{
if (!string.IsNullOrWhiteSpace(redirectUri)) HttpContext.Response.Cookies.Append("RedirectUri", redirectUri, new CookieOptions { Path = "/", Domain = AppConfiguration.ApplicationDomain, MaxAge = TimeSpan.FromDays(1) });
HttpContext.Request.Cookies.TryGetValue("RefreshToken", out string refreshToken);
HttpContext.Response.Cookies.Delete("RefreshToken", new CookieOptions { Domain = AppConfiguration.ApplicationDomain, Path = "/" });
HttpContext.Response.Cookies.Delete("BearerToken", new CookieOptions { Domain = AppConfiguration.ApplicationDomain, Path = "/" });
var logoutModel = new LogoutUserHandlerModel { RefreshToken = refreshToken };
await _logoutUserHandler.LogoutAsync(logoutModel);
return RedirectToAction("Login", "Account");
}
[HttpPost]
public async Task<IActionResult> Login(IFormCollection model)
{
var viewModel = new LoginAccountViewModel();
if (model.ContainsKey("userEmail")) viewModel.UserEmail = model["userEmail"];
if (model.ContainsKey("userPassword")) viewModel.UserPassword = model["userPassword"];
if (model.ContainsKey("redirectUri")) viewModel.RedirectUri = model["redirectUri"];
if (model.ContainsKey("rememberCheckbox")) viewModel.RememberUser = model["rememberCheckbox"] == "on";
var authenticateUserHandlerResponse = await _authenticateUserHandler.AuthenticateAsync(viewModel.UserEmail, viewModel.UserPassword);
if (authenticateUserHandlerResponse.Result != AuthenticateUserHandlerResult.Success)
{
viewModel.WrongCredentials = true;
return View(viewModel);
}
HttpContext.Response.Cookies.Append("BearerToken", authenticateUserHandlerResponse.BearerToken, new CookieOptions { Path = "/", Domain = $"{AppConfiguration.ApplicationDomain}", HttpOnly = true});
HttpContext.Response.Cookies.Append("RefreshToken", authenticateUserHandlerResponse.RefreshToken, new CookieOptions { Path = "/", Domain = $"{AppConfiguration.ApplicationDomain}", HttpOnly = true, Expires = DateTime.UtcNow.AddMonths(3)});
return Redirect(viewModel.RedirectUri);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
