AntiforgeryToken validation failed - authentication

I've ASP CORE 2.1 application that works in conjunction with angular SPA client.
I protected my application as stated here.
But the problem is that I constantly receive 400 Bad Request right after I login to the system. As per my point of view that is what happened in the system:
First request to the app -> returns AntiForgery CookieToken and
RequestToken (important note that user isn't authenticated yet)
User logins to the system -> AntiForgery validation passed,
authentication cookies sent to the client.
User requests any
other endpoint, but since the AntiforgeryTokenSet was issued for
non-authenticated user, he gets 400 Bad Request.
It is obvious, that after login we need to reissue AntiforgeryTokenSet but I've no idea where and how. I've tried to issue token in the Result Filter but with no luck.
public class SPAAntiforgeryCookieResultFilter : ResultFilterAttribute
{
private readonly IAntiforgery _antiforgery;
public SPAAntiforgeryCookieResultFilter(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
Action assignAntiForgery = () =>
{
var tokens = _antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
};
if (context.Result is ViewResult)
{
assignAntiForgery();
}
else if (string.Equals(context.ActionDescriptor.ActionName, nameof(AccountController.Login), StringComparison.OrdinalIgnoreCase))
{
assignAntiForgery();
}
}
}
It seems that ResultExecutingContext does't know about authenticated user and issues token still for anonymous user.
So, how we can refresh antiforgery RequestToken token right after login for authenticated user?

For now I find out the way how to get around the issue.
Previously, I returned Ok() after successful login, but now I do RedirectToAction() and my OnResultExecuting filter was slightly changed in order to refresh token after RedirectToAction happend.
public class SPAAntiforgeryCookieResultFilter : ResultFilterAttribute
{
private readonly IAntiforgery _antiforgery;
public SPAAntiforgeryCookieResultFilter(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
Action assignAntiForgery = () =>
{
var tokens = _antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
};
if (context.Result is ViewResult)
{
assignAntiForgery();
}
// Here we check whether our redirect action is executed. Update AFT if it is
else if (string.Equals(context.ActionDescriptor.RouteValues["action"], nameof(AccountController.RefreshAntiForgeryAfterLogin), StringComparison.OrdinalIgnoreCase))
{
assignAntiForgery();
}
}
}
This dirty hack works in the next way:
First request to the app -> returns AntiForgery CookieToken and RequestToken (important note that user isn't authenticated yet)
User press login to the system -> AntiForgery validation passed, authentication cookies set up -> User get response with authentication cookies and status code 302 (redirect) -> User reach out our RedirectUserToAction method with context that has authentication info (User.IsAuthenticated == true)

Related

ASP.NET Core 3.1 Microsoft Graph Access Token lost when application is restarted

Using ASP.NET Core 3.1 and Microsoft Graph API, when the application is restarted the Access Token is lost. The user is still logged in, but is unable to perform any Graph API calls.
I have tried the recommendations mentioned here Managing incremental consent and conditional access but was unable to refresh the token, and resume automatically.
Here is my Controller:
readonly ITokenAcquisition tokenAcquisition;
private GraphServiceClient graphServiceClient;
public HomeController(GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition)
{
this.graphServiceClient = graphServiceClient;
this.tokenAcquisition = tokenAcquisition;
}
[HttpGet]
[AuthorizeForScopes(Scopes = new string[] {"user.read"})]
public async Task<IActionResult> A()
{
User user;
try
{
var scopes = new string[] { "user.read" };
var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
user = await graphServiceClient.Me.Request().GetAsync();
}
catch (MicrosoftIdentityWebChallengeUserException ex)
{
// token is invalid....
// the throw causes a redirect to the User Login Page
throw ex.MsalUiRequiredException;
}
user = await graphServiceClient.Me.Request().GetAsync();
Serilog.Log.Debug("{#User}", user);
return View();
}
In the code above, when the application is restarted the access token is
lost, and re-throwing the exception causes a redirect to the Login-Page.
If I then click the Sign-in with Microsoft button, the user is already signed-in,
there is no need to enter the credentials. If I then access the controller calling the Graph API, the API calls succeed.
How can I refresh the tokens before calling the API?
Also how can I debug this? If I set a breakpoint at throw ex.MsalUiRequiredException; It is of no use, I cannot see,
where the redirect get's it's value from.
The Annotation [AuthorizeForScopes(Scopes = new string[] {"user.read"})]
Is responsible for dealing with the redirect.
For debugging, use the AuthorizeForScopes source file.
In this specific case the AuthenticationScheme was not set to a value,
and was null.
Annotate the Action method to enforce a specific Scheme.
e.g. AuthenticationScheme=OpenIdConnectDefaults.AuthenticationScheme
[HttpGet]
[AuthorizeForScopes(Scopes = new string[] {"user.read"}, AuthenticationScheme=OpenIdConnectDefaults.AuthenticationScheme)]
public async Task<IActionResult> A()
{
// snipped
}
This resulted into the desired redirect path:
https://login.microsoftonline.com/{}/oauth2/v2.0/authorize?client_id={}

.Net Core cannot post to Logout page after cookie expiration

When I create a new Asp.Net Core web application and
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromSeconds(5);
});
then I get logged out after 5 seconds. However, I still have the Logout button on my page at that time, if I click on the button, I get a 400 Bad Request back.
I have then tried removing the expiration code, logging out through the logout button and then using Postman to post to the logout post action but I get the same result.
[AllowAnonymous]
public class LogoutModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
{
_signInManager = signInManager;
_logger = logger;
}
public void OnGet()
{
}
//When I am logged out (whether by clicking on the logout button or after cookie expiration),
//and try to invoke this action, I get a 400 Bad Request back
public async Task<IActionResult> OnPost(string returnUrl = null)
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
return Page();
}
}
}
Does anyone know what is going on here?
EDIT: BTW, I am using .Net Core 2.2
All posts are protected by anti-forgery tokens out of the box. The anti-forgery token, when a user is logged in, is actually tied to that particular user. Since the user's authentication expires so quickly, pretty much by the time the page has loaded, the token is already invalid. It was created for a particular user, but that user is no longer authenticated. Therefore, the token validation will fail on the post request being made to logout, and you get a 400.
One quick fix would be to apply the [IgnoreAntiforgeryToken] attribute to your LogoutModel. That's technically removing a layer of security, but I can't see much that can be gained by a malicious bad actor forging a request to a logout endpoint.
However, this is mostly an artificial issue. You probably cranked down the auth cookie expiry to test what happens when it expires, but 5 seconds is not a realistic expiry. With that, a user would have to virtually reauthenticate with every request, which means they'd never really get past the login page. As soon as they logged in, they'd be expired, and have to log in again. With a more realistic number like 20 minutes, the likelihood that a user is going to sit on the same page for 20+ minutes, and then attempt to click "Logout" is rather low, making this mostly a non-issue.
On Layout page check for session expiration and call LogOff Action in Account Controller
Below script in Layout.cshtml section and add sessiontimeout in Appsettings.json
<script>
//session end
var sessionTimeoutWarning = #Configuration.GetSection("Cookie")["sessionTimeout"] - 1;
var sTimeout = parseInt(sessionTimeoutWarning) * 60 * 1000;
setTimeout('SessionEnd()', sTimeout);
function SessionEnd() {
window.location = "/Account/LogOff";
}
</script>
In the Account Controller add LogOff Action
public ActionResult LogOff()
{
_signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}

Custom Authorization Filter in .NET Core API

I want to authorize users before accessing any data using my core api, so I tried is using JWT authentication.
I have successfully generated token while signing in user using api and saved that token on client side in session, now whenever user wants to access any data using api, I'll send that token in header to api and I want to validate that JWT token using custom authorization filter. I have created custom authorization filter and applied it on my GetMenu api and I'm able to validate that token successfully but after token validation in authorization filter it is not hitting it on my GetMenu api.
Here is my AccountController code:
[Filters.Authorization]
[AllowAnonymous]
[HttpPost]
[Route("GetMenu")]
public IActionResult GetMenu(string clientid, int rolecode, string repcode)
{
//further process
}
Here is my Filters.Authorization code:
public class Authorization: AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (!ValidateToken(filterContext.HttpContext.Request.Headers["token"]))
{
filterContext.Result = new UnauthorizedResult();
}
}
}
I have breakpoints on OnAuthorization method and on GetMenu api.
I'm calling my GetMenu api through postman to test, it is successfully hitting it on OnAuthorization method in Filters.Authorization and validating my JWT Token and displays Status Code: 200 in postman but after successful token validation it should hit on GetMenu api for further data processing but it is not hitting.
What can be the issue? what am i missing? please help.
You should not set the filterContext.Result if the request is successfully authorize.
//
// Summary:
// A context for authorization filters i.e. Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
// and Microsoft.AspNetCore.Mvc.Filters.IAsyncAuthorizationFilter implementations.
public class AuthorizationFilterContext : FilterContext
{
//
// Summary:
// Gets or sets the result of the request. Setting Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext.Result
// to a non-null value inside an authorization filter will short-circuit the remainder
// of the filter pipeline.
public virtual IActionResult Result { get; set; }
}
You only need to set Result when it's failed.
public class Authorization: AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (!ValidateToken(filterContext.HttpContext.Request.Headers["token"]))
{
filterContext.Result = new UnauthorizedResult();
}
}
}

OpenIdConnect access_token size and accessing claims server side

I am trying to wrap my head around several concepts here but I don't want this question to be too broad - basically what we are trying to do is use role claims as permissions to lock down our API but I am finding that the access_token is becoming too big.
We are using OpenIddict and ASP.NET Identity 3 on the server side. We have implemented the default AspNetRoleClaims table to store our claims for each role - using them as permissions.
We lock down our API endpoints using custom policy based claims authorization as shown here:
Custom Policy Based Authorization
The main issue I am finding is that our access_token containing our claims is becoming very large. We are attempting to make the ClaimType and Value to be very small in the database to make the claims footprint smaller. We have a basic CRUD type permission scheme, so for each "module" or screen in our SPA client app, there are 4 permissions. The more modules we add to our application, the more the claims are growing in the access_token and our Authorization Bearer header is becoming very large. I am worried about this becoming not very scalable as the app grows.
So the claims are embedded in the access_token and when I hit my endpoint that is locked down with a custom Policy like this...
[Authorize(Policy="MyModuleCanRead")]
[HttpGet]
public IEnumerable<MyViewModel> Get()
I can then access my ASP.NET Identity User and User.Claims in the AuthorizationHandler.
Sorry in advance if this is an obvious question - but I am wondering - in order to get the Custom Policy Based Authorization to work - does it absolutely require the claims to be in either the id_token or the access_token in order to call the handler?
If I remove the claims from the access_token, then my AuthorizationHandler code does not get hit and I cannot access my endpoint that is locked down with my custom Policy.
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
* UPDATE *
Pintpoint's answer using Authorization handlers along with the comment on how to remove additional role claims from the cookie achieved just what I was looking for.
In case this helps anyone else - here is the code to override the UserClaimsPrincipalFactory and prevent the role claims from being written to the cookie. (I had many role claims as permissions and the cookie(s) and request headers were becoming too large)
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AppClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity(Options.Cookies.ApplicationCookieAuthenticationScheme,
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
// code removed that adds the role claims
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
return new ClaimsPrincipal(id);
}
}
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
It's definitely possible. Here's how you could do that:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Has-Edit-User-Profiles-Permission", builder =>
{
builder.RequirePermission("Edit-User-Profiles");
});
});
}
}
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string permission)
{
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
Permission = permission;
}
public string Permission { get; set; }
}
public class PermissionAuthorizationHandler :
AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly UserManager<ApplicationUser> _userManager;
public PermissionAuthorizationHandler(UserManager<ApplicationUser> userManager)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager));
}
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User == null)
{
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
{
return;
}
// Use whatever API you need to ensure the user has the requested permission.
if (await _userManager.IsInRoleAsync(user, requirement.Permission))
{
context.Succeed(requirement);
}
}
}
public static class PermissionAuthorizationExtensions
{
public static AuthorizationPolicyBuilder RequirePermission(
this AuthorizationPolicyBuilder builder, string permission)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
return builder.AddRequirements(new PermissionAuthorizationRequirement(permission));
}
}

Intercepting an encrypted login token in a request

I am working on an MVC site that has some pages that need authentication and others that don't. This is determined using the Authorize and AllowAnonymous attributes in a pretty standard way. If they try to access something restricted they get redirected to the login page.
I'm now wanting to add the functionality to automatically log them in using an encrypted token passed in the querystring (the link will be in emails sent out). So the workflow I want now is that if a request goes to a page that is restricted and there is a login token in the querystring I want it to use that token to log in. If it logs in successfully then I want it to run the original page requested with the new logged in context. If it fails to log in then it will redirect to a custom error page.
My question is where would I need to insert this logic into the site?
I have seen some suggestions on subclassing the Authorize attribute and overriding some of the methods but I'm not 100% sure how to go about this (eg what I would override and what I'd do in those overridden methods.
I've also had a look at putting the logic at a controller level but I am led to understand that the authorize attribute would redirect it away from the controller before any code in the controller itself was run.
It would be better to write a custom authorization attribute that will entirely replace the default functionality and check for the query string parameter and if present, decrypt it and authenticate the user. If you are using FormsAuthentication that would be to call the FormsAuthentication.SetAuthCookie method. Something along the lines of:
public class TokenAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
string token = filterContext.HttpContext.Request["token"];
IPrincipal user = this.GetUserFromToken(token);
if (user == null)
{
this.HandleUnAuthorizedRequest(filterContext);
}
else
{
FormsAuthentication.SetAuthCookie(user.Identity.Name, false);
filterContext.HttpContext.User = user;
}
}
private IPrincipal GetUserFromToken(string token)
{
// Here you could put your custom logic to decrypt the token and
// extract the associated user from it
throw new NotImplementedException();
}
private void HandleUnAuthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new ViewResult
{
ViewName = "~/Views/Shared/CustomError.cshtml",
};
}
}
and then you could decorate your action with this attribute:
[TokenAuthorize]
public ActionResult ProcessEmail(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}