Authorization in .NET Core 3: Order in which requirements are being executed - asp.net-core

I am imposing different policies to be satisfied in order to execute certain actions and I'd like to decide the order in which these policies have to be exectued. If a user is not authenticated, I should not check if it has permissions or not.
In startup I have:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("UserIsAuthenticated")
.RequireAuthorization("UserIsRegistered");
});
and then I use the default [Authorize(Roles = "Administrator")] attribute and my [MyAuthorize] attributes.
The order in which these polices should be executed is:
First, UserIsAuthenticated, to check if the user is authenticated.
If they are, it should check UserIsRegistered.
Finally the attributes should be applied.
In my case order matters, because I have
services.AddAuthorization(options => { options.InvokeHandlersAfterFailure = false; });
(if a user is not authenticated, I can't check their claims and it makes no sense to check the following policies).
However, in some cases I've seen that the attributes are being evaluated before the Authentication policies.
Is there a way to impose the order of the requirements?

You could create a custom AuthorizeMultiplePolicyFilter to check these policies manually.Apply the filter to global then it will be excuted before action/controller filters.
public class AuthorizeMultiplePolicyFilter: IAsyncAuthorizationFilter
{
private IAuthorizationService _authorization;
private string[] _policies;
public AuthorizeMultiplePolicyFilter(string[] policies)
{
_policies = policies;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Path.StartsWithSegments("/Account"))
{
return;
}
_authorization = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
foreach (var policy in _policies)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
if(policy == "UserIsAuthenticated")
{
context.Result = new RedirectResult("/Account/Login");
}
if(policy == "UserIsRegistered")
{
context.Result = new ForbidResult();
}
return;
}
}
}
}
Startup.cs
services.AddControllersWithViews(options =>
options.Filters.Add(new AuthorizeMultiplePolicyFilter(new string[] { "UserIsAuthenticated", "UserIsRegistered" }))
);

Related

Does IdentityServer4 still allow ResourceAuthorize?

I'm looking at upgrading from IdentityServer3 to IdentityServer4, specifically because we're upgrading existing projects from .NET 4.5 to .NET Core 3.1.
The biggest issue I see right now is that we use the ResourceAuthorize attribute to check if the user has permission against a resource
[ResourceAuthorize("Read","urn://someresource")]
But looking through the ID4 documentation and the code base, it doesn't look like ResourceAuthorize exists. The documentation does show examples of using Authorize, but I'm not seeing anything that lets me check for a permission against a resource.
Has the paradigm changed or is there another way to get this type of check done with ID4?
You can add policies:
Startup.cs
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"SomePolicy",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(
new SomePolicyRequirement());
});
});
SomePolicyRequirement.cs
public class SomePolicyRequirement : IAuthorizationRequirement
{
public SomePolicyRequirement()
{
}
}
SomePolicyHandler.cs
public class SomePolicyHandler : AuthorizationHandler<SomePolicyRequirement>
{
public SomePolicyHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SomePolicyRequirement requirement)
{
var endpoint = context.Resource as Endpoint;
if (endpoint == null)
{
context.Fail();
return Task.CompletedTask;
}
/*
//RouteData can be controller, action or id
var imageId = filterContext.RouteData.Values["id"].ToString();
if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
{
context.Fail();
return Task.CompletedTask;
}*/
/*
//Repository check can go here
var ownerId = context.User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
if (!_someRepository.IsImageOwner(imageIdAsGuid, ownerId))
{
context.Fail();
return Task.CompletedTask;
}*/
// all checks out
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Dotnet core added some great features for authorization. Resource base authorization can implemented very easy with policy based authorization.
Policy-based authorization

handle not authorized to OpenID connect policy?

I am following this wiki Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app
I have a policy like this :
services.AddAuthorization(options =>
{
options.AddPolicy("CanAccessAdminGroup",
policyBuilder => policyBuilder.RequireClaim("groups", "Guid"));
});
My controller is decorated with [Authorize(Policy = "CanAccessAdminGroup")]
Which works ok when user is in this AAD group.
But when user is not in group, i get sent to xxx/Account/AccessDenied?returnurl=xx
How do I change the redirect to use a different controller/action, like /identity/index ?
I tried to this but did not work:
OnAuthenticationFailed = context =>
{
context.Response.Redirect("Identity/Index");
context.HandleResponse(); // Suppress the exception
return Task.CompletedTask;
This is the output from Debug window:
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ForbidResult:Information: Executing ForbidResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: AzureADCookie was forbidden.
You can firstly create an authorization requirement :
public class MatchGroupRequirement : IAuthorizationRequirement
{
public String GroupID { get; }
public MatchGroupRequirement(string groupID)
{
GroupID = groupID;
}
}
Create an authorization handler which is responsible for the evaluation of a requirement's properties , in custom authorization you can redirect to any desired controller action using the AuthorizationFilterContext and with the RedirectToActionResult :
public class MatchGroupHandler : AuthorizationHandler<MatchGroupRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
MatchGroupRequirement requirement)
{
var redirectContext = context.Resource as AuthorizationFilterContext;
var groups = context.User.Claims.Where(c => c.Type == "groups").ToList();
var matchingvalues = groups.Where(stringToCheck => stringToCheck.Value.Contains(requirement.GroupID)).FirstOrDefault();
//check the condition
if (matchingvalues == null)
{
redirectContext.Result = new RedirectToActionResult("identity", "index", null);
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Policy and handler registration :
services.AddAuthorization(options =>
{
options.AddPolicy("MatchGroup", policy =>
policy.Requirements.Add(new MatchGroupRequirement("ddf1ad17-5052-46ba-944a-7da1d51470b0")));
});
services.AddSingleton<IAuthorizationHandler, MatchGroupHandler>();
Applying policies to MVC controllers/Actions :
[Authorize(Policy = "MatchGroup")]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}

TenantId Claim not being returned from IdentityServer4 to Client App

Following on from this question, I ended up using the HttpContext.SignInAsync(string subject, Claim[] claims) overload to pass the selected tenant id as a claim (defined as type "TenantId") after the user selects a Tenant.
I then check for this claim in my custom AccountChooserResponseGenerator class from this question to determine if the user needs to be directed to the Tenant Chooser page or not, as follows:
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var response = await base.ProcessInteractionAsync(request, consent);
if (response.IsConsent || response.IsLogin || response.IsError)
return response;
if (!request.Subject.HasClaim(c=> c.Type == "TenantId" && c.Value != "0"))
return new InteractionResponse
{
RedirectUrl = "/Tenant"
};
return new InteractionResponse();
}
The interaction is working and the user gets correctly redirected back to the Client app after selecting a Tenant.
However, on my client, I have the simple:
<dl>
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
snippet from the IdentityServer4 quickstarts to show the claims, and sadly, my TenantId claim is not there.
I have allowed for it in the definition of my Client on my IdentityServer setup, as follows:
var client = new Client
{
... other settings here
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Phone,
"TenantId"
}
};
What am I missing in order for this TenantId claim to become visible in my Client application?
EDIT:
Based on #d_f's comments, I have now added TentantId to my server's GetIdentityResources(), as follows:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResource("TenantId", new[] {"TenantId"})
};
}
And I have edited the client's startup.ConfigureServices(IServiceCollection services) to request this additional scope, as follows:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
//other settings not shown
options.Scope.Add("TenantId");
});
And still the only claims displayed on the client by the indicated snippet are:
Edit 2: Fixed!
Finally #RichardGowan's answer worked. And that is because (as brilliantly observed by #AdemCaglin) I was using IdentityServer's AspNetIdentity, which has it's own implementation of IProfileService, which kept dropping my custom TenantId claim, despite ALL these other settings).
So in the end, I could undo all those other settings...I have no mention of the TenantId claim in GetIdentityResources, no mention of it in AllowedScopes in the definition of the Client in my IdSrv, and no mention of it in the configuration of services.AddAuthentication on my client.
You will need to provide and register an implementation of IProfileService to issue your custom claim back to the client:
public class MyProfileService : IProfileService {
public MyProfileService() {
}
public Task GetProfileDataAsync(ProfileDataRequestContext context) {
// Issue custom claim
context.IssuedClaims.Add(context.Subject.Claims.First(c => c.Type ==
"TenantId"));
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context) {
context.IsActive = true;
return Task.CompletedTask;
}
}

How to configure JwtBearer with mandatory claim?

My application logic depends on a claim existing, hence this claim is mandatory and needs to always be present in the token.
I am not interested in a Authorization Policy since policies applies to different users and this is a mandatory claim required to be present in all tokens.
Right now my controllers contains:
private const string MyCustomClaim = "foo";
private string _myCustomClaim;
public override void OnActionExecuting(ActionExecutingContext context)
{
_myCustomClaim = context.HttpContext.User.FindFirst(MyCustomClaim)?.Value;
}
If the field _myCustomClaim is null then things will fail later.
I could add a null check and throw an exception, but it would be better if the Authorization middleware did not authorize the user if the token did not contain the claim.
Is there any way to inform the Authorization middleware that a certain claim is mandatory?
In the Startup.cs file when configuring the authentication middleware handle the OnTokenValidated event.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
const string claimTypeFoo = "foo";
if (!context.Principal.HasClaim(c => c.Type == claimTypeFoo))
{
context.Fail($"The claim '{claimTypeFoo}' is not present in the token.");
}
return Task.CompletedTask;
}
};
});
This could also be done in a class:
File Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Events = new MyJwtBearerEvents();
});
File MyJwtBearerEvents.cs
public class MyJwtBearerEvents : JwtBearerEvents
{
private const string ClaimTypeFoo = "foo";
public override Task TokenValidated(TokenValidatedContext context)
{
if (!context.Principal.HasClaim(c => c.Type == ClaimTypeFoo))
{
context.Fail($"The claim '{ClaimTypeFoo}' is not present in the token.");
}
return Task.CompletedTask;
}
}

ASP.NET Core Windows Authentication and Application Roles

I'm trying to create a fairly simple intranet application that will use Active Directory for authentication, and will use the AspNetRoles table to check if the user is in a certain application role. This app is just an in-house lottery where some users can create events/contests that other users can then submit an entry to the contest. I'm thinking of starting out with 2 basic roles:
Administrator - Can perform CRUD operations on "Event" or
"Contest" entities
Contestant - Can perform GET operations on
"Contest" entities, and can create new "Entry" entities.
Here's where I'm stuck: I've got Windows Authentication working in the sense that from a controller, I can do a User.Identity.Name and see my domain login name. Furthermore, I can verify that an account belongs to a domain group by doing User.IsInRole("Domain Users"). If I want to avoid creating new AD groups for each role in my application (let's say design changes down the road require additional roles), how can I use Authorization on controllers to check against Application Roles?
Here's an example controller I want to use:
[Route("api/[controller]")]
[Authorize(Roles = "Contestant")]
public class EventTypesController : Controller
{
private IRaffleRepository _repository;
private ILogger<EventTypesController> _logger;
public EventTypesController(IRaffleRepository repository, ILogger<EventTypesController> logger)
{
_repository = repository;
_logger = logger;
}
[HttpGet("")]
public IActionResult Get()
{
try
{
var results = _repository.GetAllEventTypes();
return Ok(Mapper.Map<IEnumerable<EventTypeViewModel>>(results));
}
catch (Exception ex)
{
_logger.LogError($"Failed to get all event types: {ex}");
return BadRequest("Error occurred");
}
}
}
In my Startup.cs, in ConfigureServices, I'm wiring up Identity as follows:
services.AddIdentity<RaffleUser, ApplicationRole>()
.AddEntityFrameworkStores<RaffleContext>();
My RaffleUser class is really just the default implementation of IdentityUser:
public class RaffleUser : IdentityUser
{
}
My ApplicationRole class is also just the default implementation of IdentityRole. I also tried seeding some data in a seed class:
if (!await _roleManager.RoleExistsAsync("Administrator"))
{
var adminRole = new ApplicationRole()
{
Name = "Administrator"
};
await _roleManager.CreateAsync(adminRole);
await _context.SaveChangesAsync();
}
if (await _userManager.FindByNameAsync("jmoor") == null)
{
using (var context = new PrincipalContext(ContextType.Domain))
{
var principal = UserPrincipal.FindByIdentity(context, "DOMAIN\\jmoor");
if (principal != null)
{
var user = new RaffleUser()
{
Email = principal.EmailAddress,
UserName = principal.SamAccountName
};
await _userManager.CreateAsync(user);
await _context.SaveChangesAsync();
var adminRole = await _roleManager.FindByNameAsync("Administrator");
if (adminRole != null)
{
await _userManager.AddToRoleAsync(user, adminRole.Name);
await _context.SaveChangesAsync();
}
}
}
}
The data makes it to the tables, but it just seems like at the controller level, I need to convert the authenticated user to an IdentityUser. Do I need some middleware class to do this for me? Would that be the best way to make authorization reusable on all controllers?
First, I ended up creating a custom ClaimsTransformer that returns a ClaimsPrincipal populated with UserClaims and RoleClaims (after refactoring my app, I decided to go with policy-based authorization, and the access claim can be added at either the role or user level):
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var identity = (ClaimsIdentity)context.Principal.Identity;
var userName = identity.Name;
if (userName != null)
{
var user = await _userManager.FindByLoginAsync("ActiveDirectory", userName);
if (user != null)
{
identity.AddClaims(await _userManager.GetClaimsAsync(user));
var roles = await _userManager.GetRolesAsync(user);
identity.AddClaims(await GetRoleClaims(roles));
}
}
return context.Principal;
}
private async Task<List<Claim>> GetRoleClaims(IList<string> roles)
{
List<Claim> allRoleClaims = new List<Claim>();
foreach (var role in roles)
{
var rmRole = await _roleManager.FindByNameAsync(role);
var claimsToAdd = await _roleManager.GetClaimsAsync(rmRole);
allRoleClaims.AddRange(claimsToAdd);
}
return allRoleClaims;
}
I wired that up in the Startup.cs:
services.AddScoped<IClaimsTransformer, Services.ClaimsTransformer>();
I also went with Policy-based authorization:
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", policy => policy.RequireClaim("AccessLevel", "Administrator"));
options.AddPolicy("Project Manager", policy => policy.RequireClaim("AccessLevel", "Project Manager"));
});
So, users or roles can have a claim set with a name of "AccessLevel" and a value specified. To finish everything off, I also created a custom UserManager that just populates the User object with additional details from ActiveDirectory during a CreateAsync.
You need to add a DefaultChallangeScheme to use Windows authentication. This is how i do, but if someone has a better solution i am all ears :)
I use the following setup in my current application.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<SecurityDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
});
Then i put in my application claims in a transformer.
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
I hope this will get you in the right direction.