Extend Authorize attribute .Net Web API - c#

A lot of my methods in my WebAPI looks like this. I have an authorization-filter on policy, but then i also need to validate my token and that the user provided in the token exists in DB. I am wondering if there is a way of extending the Authorize attribute so that after policy is validated, the token gets validated too. And is it also possible to return the user into the method or context if existing?
[Authorize(Policy = "Admin")]
[Route("get")]
[HttpGet]
public async Task<IActionResult> Get()
{
var (success, user) = await UserHelpers.ValidateTokenAndGetUserAsync(HttpContext, _userManager, _configuration, _logger, ControllerContext, ModelState);
if (!success || user?.CustomerId == null) return Unauthorized(HttpResponseHelper.GetErrorResponse(StatusCodes.Status401Unauthorized, _localizer[ResourceConstants.Unauthorized.UserNotFoundError].Value));
//Code
await UserHelpers.CookieToResponse(user, _configuration, _webHostEnvironment, Response, Request).ConfigureAwait(false);
return Ok();
}

First, I should address that you shouldn't customize Authorize attribute.
Using (Policy, Claim, Requirement) can help you to customize your authorization based on your business.
Then my question is that your user was in the database while issuing the token. So how can it be removed now? (The user in the JWT-Token is being validated via the signature of that)
If you want to double-check if the user is enabled, or not archived yet you can write another filter like this:
public class ApiController : Controller
{
[Authorize(Policy = "Bla")]
[CheckUserIsEnabled]
public async Task<IActionResult> Get()
{
return Ok();
}
}
public class CheckUserIsEnabledAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var userId = context.HttpContext.User.FindFirst(x => x.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
if (userId is null)
{
context.Result = new UnauthorizedResult();
return;
}
bool exists = true;
//exists = db.CheckIfUserExistsAsync(userId);
if (!exists)
{
context.Result = new UnauthorizedResult();
return;
}
await next();
}
}
This solution works because the JWT claims will be added to current user claims on httpRequest instantiation (even if you don't put Authorize attribute on the corresponding action)

Related

How to check authorize based on field?

I have a controller method and usually check make Authorize like this:
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "Policy")]
public async Task<IActionResult> Get(SomeType someObject)
{
return Ok();
}
Question: I need to Authorize based on RequireAuth field in someObject(passed in method), for example, if someObject.RequireAuth == true, then we need to apply
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "Policy")]
If someObject.RequireAuth == false, then [AllowAnonymous]

Authorization in ASP .NET Core Razor pages

I am unable to implement policy-based authorization in ASP .NET Core for an action on a razor page.
I read through this comprehensive document on authorization and used its examples as guidence.
Razor page action code:
[Authorize(Policy = "test")]
public async Task<IActionResult> OnGetCreateAsync(string id)
Code in service configuration:
_ = services.AddAuthorization(options => {
options.AddPolicy("test", policy =>
policy.RequireAssertion(context =>
false));
});
I expect that if I call the action or endpoint service, e.g.
GET /Account?handler=Create
then the request will be denied with a 403 status response because the "test" policy states that everyone is unauthorized. However, in actual practice, the action is successfully called.
Razor Pages doesn't support [Authorize] at the handler level. i.e. You can only authorise a page as a whole, on the PageModel itself, as noted in the docs:
Policies can not be applied at the Razor Page handler level, they must be applied to the Page.
If authorising the page as a whole isn't a workable solution, you might need to move your OnGetCreateAsync handler into a controller/action pair, which can be attributed with [Authorize] accordingly.
There's also a related GitHub issue in the docs for this:
The [Authorize] filter attribute has been supported since 2.0 in Razor Pages, but note that it works at the page model class level
If you need a better workaround, see akbar's answer and Jim Yabro's answer.
Another solution for it is to check authentication by if clause.like this:
if (!HttpContext.User.Identity.IsAuthenticated)
{
return Redirect("/Front/Index");
}
and you can check roles in it too by finding role:
var user = await _userManager.FindByEmailAsync(model.Email);
var roles = await _userManager.GetRolesAsync(user);
I would recommend following Razor Pages authorization conventions in ASP.NET Core that looks like this:
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Contact");
options.Conventions.AuthorizeFolder("/Private");
options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
In your case with policy test it would look like this:
options.Conventions.AuthorizePage("/Account", "test");
Source:
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-5.0
Authorize attribute is supported but only on PageModel like this:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PageFilter.Pages
{
[Authorize]
public class ModelWithAuthFilterModel : PageModel
{
public IActionResult OnGet() => Page();
}
}
Source:
https://learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-5.0#authorize-filter-attribute
Don't use the AuthorizeAttribute since it's unsupported.
Instead, once you have configured your policies in Startup.cs, you will be able to check those policies from within the page handlers.
Inject IAuthorizationService into your page model constructor
Call AuthorizeAsync() from within the handler.
Run a conditional check on the result's .Succeeded property.
If .Succeeded is false, return a Forbid() result.
This has nearly the same result as the [Authorize(Policy=...)] but executes later in the page lifecycle.
using Microsoft.AspNetCore.Authorization;
// ...
public class TestPageModel : PageModel {
readonly IAuthorizationService AuthorizationService;
public TestPageModel(IAuthorizationService authorizationService) {
AuthorizationService= authorizationService;
}
// Everyone can see this handler.
public void OnGet() { }
// Everyone can access this handler, but will be rejected after the check.
public async Task<IActionResult> OnPostAsync() {
// This is your policy you've defined in Startup.cs
var policyCheck = await AuthorizationService.AuthorizeAsync(User, "test");
// Check the result, and return a forbid result to the user if failed.
if (!policyCheck.Succeeded) {
return Forbid();
}
// ...
return Page(); // Or RedirectToPage etc
}
}
my workaround using Permission-based Authorization in ASP.NET Core :
[Authorize(Permissions.PageX.AddParameter)]
public async Task<IActionResult> OnPostAddParameterAsync(uint id, string key, string value)
{
if (!this.ArePermissionsValid()) return Forbid();
/// ...
}
public static class PageExtensions
{
public static bool ArePermissionsValid(this PageModel page)
{
try
{
var authorizeAttribute = new StackTrace().GetFrames().FirstOrDefault(x => x.GetMethod().Name.StartsWith("On")).GetMethod().GetCustomAttribute<AuthorizeAttribute>();
if (authorizeAttribute == null) return true;
var hasPermissions = page.User.Claims.Any(x => x.Type.Equals("permission") && x.Value.Equals(authorizeAttribute.Policy));
return hasPermissions;
}
catch (Exception e)
{
Log.Error($"${nameof(PageExtensions)}.{nameof(ArePermissionsValid)} | {e.Message}");
return false;
}
}
}

Catching a failed authorization policy

In .Net Core 2, suppose there is an authorization policy that checks that specific claims exist for the user:
public class CompletedProfileRequirement : AuthorizationHandler<CompletedProfileRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CompletedProfileRequirement requirement)
{
// Check that all required claims exist
if (/* invalid user claims */) {
context.Fail();
}
context.Succeed(requirement);
return Task.FromResult(0);
}
}
And we stick the policy check on top of an MVC controller:
[Authorize(Policy = "CompletedProfile")]
public class HomeController : Controller
{
// Controller stuff
}
How can I write an IAuthorizationFilter so that we can catch a failure on this particular CompletedProfileRequirement policy so that we can redirect the user to a page where they may complete their profile?
According to the authorization documentation, it's possible to access the MVC context from your AuthorizationHandler:
Frameworks such as MVC or Jabbr are free to add any object to the Resource property on the AuthorizationHandlerContext to pass extra information.
For example, MVC passes an instance of AuthorizationFilterContext in the Resource property. This property provides access to HttpContext, RouteData, and everything else provided by MVC and Razor Pages.
Therefore, you can do something like this:
public class CompletedProfileRequirement : AuthorizationHandler<CompletedProfileRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CompletedProfileRequirement requirement)
{
// Check that all required claims exist
if (/* invalid user claims */) {
// retrieve MVC context
if (context.Resource is AuthorizationFilterContext mvcContext)
{
// we still need to mark the requirement as succeeded,
// otherwise responde code is 401 unauthorized
context.Succeed(requirement);
// HTTP 302 temporary redirect to your page
mvcContext.HttpContext.Response.Redirect("/Home/Wherever", false);
return Task.FromResult(0);
}
}
context.Succeed(requirement);
return Task.FromResult(0);
}
}
You should add your policy with requirments to authorization on application startup, like:
services.AddAuthorization(options =>
{
options.AddPolicy("CompletedProfile", builder =>
{
builder.AddRequirements(new CompletedProfileRequirement());
});
});
And register handlers, like:
services.AddScoped<IAuthorizationHandler, CompletedProfileRequirementHandler>();
UPD:
For redirecting you should inject and use in controller:
IAuthorizationService authorizationService
And invoke:
var result = _authorizationService.AuthorizeAsync(User, someObject, "CompletedProfile");
if (!result.Succeeded)
return RedirectToAction("CompleteProfileAction");

Asp.Net WebApi 2: Add role claim to WindowsIdentity

I want to add a role claim to an already authenticated windows user. My naive first approach was to add the role claim inside of a custom owin middleware which runs before WebApi. Like this:
public class IdentityMiddleware : OwinMiddleware
{
public IdentityMiddleware(OwinMiddleware next) : base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
var user = context.Request.User as WindowsPrincipal;
var identity = user.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
await Next.Invoke(context);
}
}
However when providing the Authorize attribute in the controller like this.
public class TestController : ApiController
{
[Authorize(Roles = "Admin")]
public string Get()
{
return User.Identity.Name;
}
}
..I will get a 401.
I noticed that the issuer of the new claim is "Local Authority" instead of "AD Authority" could this be the reason?
Have you tried this for your authorize attribute:
[Authorize(ClaimTypes.Role, "Admin")]
This works for me:
var сlaimsIdentity = user.Identity as ClaimsIdentity;
сlaimsIdentity?.AddClaim(new Claim(сlaimsIdentity.RoleClaimType, "Admin"));
if (user.IsInRole("Admin")) ... // always true

Refresh user cookie ticket in ASP.Net Core Identity

In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.
The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.
The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
// todo: update the Client Cookie
await _signInManager.RefreshSignInAsync(User); // wrong type
}
How do I refresh the cookie?
public static class HttpContextExtensions
{
public static async Task RefreshLoginAsync(this HttpContext context)
{
if (context.User == null)
return;
// The example uses base class, IdentityUser, yours may be called
// ApplicationUser if you have added any extra fields to the model
var userManager = context.RequestServices
.GetRequiredService<UserManager<IdentityUser>>();
var signInManager = context.RequestServices
.GetRequiredService<SignInManager<IdentityUser>>();
IdentityUser user = await userManager.GetUserAsync(context.User);
if(signInManager.IsSignedIn(context.User))
{
await signInManager.RefreshSignInAsync(user);
}
}
}
Then use it in your controller
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
await HttpContext.RefreshLoginAsync();
}
Or abstract it in an action filter
public class RefreshLoginAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await context.HttpContext.RefreshLoginAsync();
await next();
}
}
Then use it like this in your controller
[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
// your normal controller code
}
This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).
Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.
We use the following flow (js client app connecting to .NET Core API):
User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
The client receives the token, userID, and refreshToken and stores them in localStorage.
When the original token expires (1 hour), the client gets a 401 error from the API.
The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
The client the repeats the call that originally errored in a 401 with the new token.
Here is the code we added to the User Service:
public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
try
{
var result = await _cognitoIdentityProvider.InitiateAuthAsync(
new InitiateAuthRequest
{
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
ClientId = _pool.ClientID,
AuthParameters = new Dictionary<string, string>
{
{ "REFRESH_TOKEN", refreshToken },
{ "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
}
});
if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
return new UserLoginResult(UserLoginStatus.Failed);
return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
}
catch
{
return new UserLoginResult(UserLoginStatus.Failed);
}
}
private static string HmacSHA256(string data, string key)
{
using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
{
var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(result);
}
}
IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.
AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.
UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.
Please note that setting SECRET_HASH may not be required based on your config is Cognito.

Categories