handle not authorized to OpenID connect policy? - asp.net-core

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();
}

Related

Authorization in .NET Core 3: Order in which requirements are being executed

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" }))
);

Adding claims to IdentityServer setup by AddIdentityServer

I have a SPA that has an ASP.NET Core web API together with the inbuilt identity server switched on using AddIdentityServer and then AddIdentityServerJwt:
services.AddIdentityServer()
.AddApiAuthorization<User, UserDataContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
I also have an authorization policy setup that requires an "Admin" role claim:
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy => policy.RequireClaim(ClaimTypes.Role, "Admin"));
});
I have a controller action that uses this policy
[Authorize(Policy = "IsAdmin")]
[HttpDelete("{id}")]
public IActionResult Deleten(int id)
{
...
}
The authenticated user does have the "Admin" role claim:
The access token for this authentication user doesn't appear to contain the admin claim:
I get a 403 back when trying to request this resource with the admin user:
So, if I'm understanding this correctly, IdentityServer isn't including the admin role claim and so the user isn't authorized to access the resource.
Is it possible to configure the claims that IdentityServer uses using AddIdentityServerJwt? or am I misunderstanding why this is not working.
One of the other answers is really close to the specific use case in question but misses the point about it being SPA.
Firstly you must add your IProfileService implementation like suggested already:
public class MyProfileService : IProfileService
{
public MyProfileService()
{ }
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//get role claims from ClaimsPrincipal
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
//add your role claims
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
// await base.IsActiveAsync(context);
return Task.CompletedTask;
}
}
But then go ahead and do this:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddProfileService<MyProfileService>();
And your claim will be exposed on the JWT. Replace the ClaimTypes.Role constant with any string corresponding to the claim type you want to expose.
On Identity Server side , you can create Profile Service to make IDS4 include role claim when issuing tokens .
You can get role claims from ClaimsPrincipal or get the roles from database and create profile service like :
public class MyProfileService : IProfileService
{
public MyProfileService()
{ }
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//get role claims from ClaimsPrincipal
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
//add your role claims
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
// await base.IsActiveAsync(context);
return Task.CompletedTask;
}
}
And register in Startup.cs:
services.AddTransient<IProfileService, MyProfileService>();
On client side , you should map the role claim from your JWT Token and try below config in AddOpenIdConnect middleware :
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters.RoleClaimType = "role";
Then your api could validate the access token and authorize with role policy .
I did this without using roles but with using a special claim added to the users token. I have created a CustomUserClaimsPrincipalFactory this allows me to add additional claims to the user.
register
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>();
the code.
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole<long>>
{
public CustomUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<long>> roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity("Identity.Application",
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, user.Name));
id.AddClaim(new Claim("preferred_username", userName));
id.AddClaim(new Claim("culture", user.Culture ?? "da-DK"));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
if(user.IsXenaSupporter)
id.AddClaim(new Claim("supporter", user.Id.ToString()));
return id;
}
}
policy
services.AddAuthorization(options =>
{
options.AddPolicy("Supporter", policy => policy.RequireClaim("supporter"));
});
usage
[Authorize(AuthenticationSchemes = "Bearer", Policy = "Supporter")]
[HttpPost("supporter")]
public async Task<ActionResult> ChangeToSpecificUser([FromBody] ChangeUserRequest request)
{
// ..................
}

How to force re authentication between ASP Net Core 2.0 MVC web app and Azure AD

I have an ASP.Net Core MVC web application which uses Azure AD for authentication. I have just received a new requirement to force user to reauthenticate before entering some sensitive information (the button to enter this new information calls a controller action that initialises a new view model and returns a partial view into a bootstrap modal).
I have followed this article which provides a great guide for achieving this very requirement. I had to make some tweaks to get it to work with ASP.Net Core 2.0 which I think is right however my problems are as follows...
Adding the resource filter decoration "[RequireReauthentication(0)]" to my controller action works however passing the value 0 means the code never reaches the await.next() command inside the filter. If i change the parameter value to say 30 it works but seems very arbitrary. What should this value be?
The reauthentication works when calling a controller action that returns a full view. However when I call the action from an ajax request which returns a partial into a bootstrap modal it fails before loading the modal with
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'https://localhost:44308' is therefore not allowed
access
This looks like a CORS issue but I don't know why it would work when going through the standard mvc process and not when being called from jquery. Adding
services.AddCors();
app.UseCors(builder =>
builder.WithOrigins("https://login.microsoftonline.com"));
to my startup file doesn't make any difference. What could be the issue here?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Ommitted for clarity...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.AddCors();
// Ommitted for clarity...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Ommitted for clarity...
app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
AzureAdAuthenticationBuilderExtensions.cs
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("auth_time");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
});
return builder;
}
private static Task RedirectToIdentityProvider(RedirectContext context)
{
// Force reauthentication for sensitive data if required
if (context.ShouldReauthenticate())
{
context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
}
else
{
context.Properties.RedirectUri = new PathString("/Account/SignedIn");
}
return Task.FromResult(0);
}
internal static bool ShouldReauthenticate(this RedirectContext context)
{
context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);
bool shouldReauthenticate = false;
if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
{
throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
}
return shouldReauthenticate;
}
// Ommitted for clarity...
}
RequireReauthenticationAttribute.cs
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst("auth_time")?.Value, out int authTime);
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (foundAuthTime && ts - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));
}
}
}
CreateNote.cs
[HttpGet]
[RequireReauthentication(0)]
public IActionResult CreateNote(int id)
{
TempData["IsCreate"] = true;
ViewData["PostAction"] = "CreateNote";
ViewData["PostRouteId"] = id;
var model = new NoteViewModel
{
ClientId = id
};
return PartialView("_Note", model);
}
Razor View (snippet)
<a asp-controller="Client" asp-action="CreateNote" asp-route-id="#ViewData["ClientId"]" id="client-note-get" data-ajax="true" data-ajax-method="get" data-ajax-update="#client-note-modal-content" data-ajax-mode="replace" data-ajax-success="ShowModal('#client-note-modal', null, null);" data-ajax-failure="AjaxFailure(xhr, status, error, false);"></a>
All help appreciated. Thanks
The CORS problem is not in your app.
Your AJAX call is trying to follow the authentication redirect to Azure AD,
which will not work.
What you can do instead is in your RedirectToIdentityProvider function, check if the request is an AJAX request.
If it is, make it return a 401 status code, no redirect.
Then your client-side JS needs to detect the status code, and issue a redirect that triggers the authentication.

How to authorize SignalR Core Hub method with JWT

I am using JWT authentication in my ASP.NET Core 2.0 application with OpenIddict.
I am following idea in this thread and calling AuthorizeWithJWT method after SignalR handshake. But now, I do not know what should I set in AuthorizeWithJWT method so I can use [Authorize(Roles="Admin")] for example.
I tried with setting context user, but it is readonly:
public class BaseHub : Hub
{
public async Task AuthorizeWithJWT(string AccessToken)
{
//get user claims from AccesToken
this.Context.User = user; //error User is read only
}
}
And using authorize attribute:
public class VarDesignImportHub : BaseHub
{
[Authorize(Roles = "Admin")]
public async Task Import(string ConnectionString)
{
}
}
I strongly encourage you to continue doing authentication at the handshake level instead of going with a custom and non-standard solution you'd implement at the SignalR level.
Assuming you're using the validation handler, you can force it to retrieve the access token from the query string:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddOAuthValidation(options =>
{
options.Events.OnRetrieveToken = context =>
{
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
};
});
}
Or OnMessageReceived if you want to use JWTBearer:
services.AddAuthentication()
.AddJwtBearer(o =>
{
o.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
if (context.Request.Path.ToString().StartsWith("/HUB/"))
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
},
};
});
No other change should be required.

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;
}
}