How to authorize SignalR Core Hub method with JWT - authentication

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.

Related

How to configure Anti-Forgery Protection in a view-less Web API

I'm implementing a REST API using ASP.NET Core. It is stateless except for the fact that is uses cookies for authentication and therefore is vulnerable to cross-site request forgery (CSRF) attacks.
Luckily, ASP.NET Core provides means as a protection against that: Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core.
As my application does not have any views or pages, I'm only configuring my controllers using services.AddControllers() in my Startup.
When hitting a REST endpoint that is attributed with [ValidateAntiForgeryToken], I get the following exception:
System.InvalidOperationException: No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter' has been registered.
Registering my controllers using services.AddControllersWithViews() makes this error go away as it internally registers the appropriate service.
According to the docs:
Antiforgery middleware is added to the Dependency injection container when one of the following APIs is called in Startup.ConfigureServices:
AddMvc
MapRazorPages
MapControllerRoute
MapBlazorHub
All of these method seem to me to be view-centric (except MapControllerRoute which I'm doing in the Configure method in my Startup but it doesn't help) and part of the namespace of the missing service is ViewFeatures. This confuses me because in my understanding, and need to take care of CSRF although I'm developing a pure Web API without views.
Is my understanding wrong? How is CSRF protection configured when no views are involved?
I will suggest move away from the default ValidateAntiForgeryToken attribute
All the harder work is done by services.AddAntiforgery(), and the ValidateAntiForgeryToken just calls antiforgery.ValidateRequestAsync()
You can create your own filter for it and register it etc. but take a look at this neat implementation, you can simply inject an instance of IAntiforgery in all the POST api methods
https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/anti-request-forgery/sample/AngularSample/Startup.cs
Here are what I believe to be bits of the Microsoft docs you link to on how to handle this. They say that "using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach." They also go on to say that the approach is to use middleware to generate an antiforgery token and send it in the response as a cookie. In short they are saying if you have an API put the antiforgery token in a cookie.
As you say with just AddControllers you can't use the [ValidateAntiForgeryToken]. As LarryX says the thing to do is create your own filter.
In case it helps anyone I have created a demo app that uses some custom middleware to check for the antiforgery token if the request is not a GET.
Note that the CORS code is just there so that I could make a post from another domain to test the code works (I tested with https://localhost:44302).
Standard Program.cs (nothing interesting here)
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace SpaAntiforgery
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Startup.cs
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace SpaAntiforgery
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
}
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
//CORS code that is needed if you want another domain to access your API
app.UseCors(
options => options.WithOrigins("https://localhost:44302")
.AllowAnyMethod()
.AllowCredentials()
.WithHeaders("x-csrf-token", "content-type"));
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//this bit is straight form the Microsoft docs. See the link reference at the start of my answer
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
return next(context);
});
//this is my custom middleware that will test for the antiforgery token if the request is not a GET
app.EnsureAntiforgeryTokenPresentOnPosts();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapFallbackToController("Index", "Home");
});
}
}
}
Here is the custommiddleware code that is needed for app.EnsureAntiforgeryTokenPresentOnPosts();
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;
namespace SpaAntiforgery
{
public class AppEnsureAntiforgeryTokenPresentOnPostsMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public AppEnsureAntiforgeryTokenPresentOnPostsMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task Invoke(HttpContext httpContext)
{
var notAGetRerquest = !string.Equals("GET", httpContext.Request.Method, StringComparison.OrdinalIgnoreCase);
if (notAGetRerquest)
{
// This will throw if the token is invalid.
await _antiforgery.ValidateRequestAsync(httpContext);
}
await _next(httpContext);
}
}
public static class AppEnsureAntiforgeryTokenPresentOnPostsExtension
{
public static IApplicationBuilder EnsureAntiforgeryTokenPresentOnPosts(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<AppEnsureAntiforgeryTokenPresentOnPostsMiddleware>();
}
}
}
HomeController.cs
The idea is to make a get to this endpoint so that your client code can retrieve the antiforgery token.
using Microsoft.AspNetCore.Mvc;
namespace SpaAntiforgery.Controllers
{
[Route("[controller]")]
[ApiController]
public class HomeController: ControllerBase
{
public IActionResult Index()
{
return Ok();
}
}
}
I also included a controller to test out a post.
using Microsoft.AspNetCore.Mvc;
namespace SpaAntiforgery.Controllers
{
[Route("[controller]")]
[ApiController]
public class TestAntiforgeryController : ControllerBase
{
[HttpPost]
public IActionResult Index()
{
return Ok();
}
}
}
Sending a post request to /testantiforgery using something like Postman results in an error because the post does not include the antiforgery token. This is what we want.
In order to test that a successful post can be made I created another website with the following code. Note the getCookie method comes straight from the Microsoft docs that I linked to at the start of my answer.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="MyButton">
Test
</button>
<script>
const getCookie = cookieName => {
var name = cookieName + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
};
const getCsrfToken = () => {
return getCookie("CSRF-TOKEN");
};
const getHeadersIncludingCsrfToken = () => {
const defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json"
};
return { ...defaultHeaders, "X-CSRF-TOKEN": getCsrfToken()};
};
const sendRequest = async (url, settings, done) => {
const baseUrl = "https://localhost:44333";
const response = await fetch(baseUrl + url, settings);
if (response.status !== 200) {
console.log("there was an api error");
return;
}
done();
};
const sendGet = async (url, done) => {
const settings = {
method: "GET"
};
await sendRequest(url, settings, done);
};
const sendPost = async (url, done) => {
const settings = {
method: "POST",
headers: getHeadersIncludingCsrfToken()
};
settings.credentials = "include";
await sendRequest(url, settings, done);
};
const sendAPost = () => {
sendPost("/testantiforgery", () => console.log("post succeeded!"));
}
const onTest = () => {
//sending a get to / means the antiforgery cookie is sent back
sendGet("/", sendAPost);
};
const MyButton = document.getElementById("MyButton");
MyButton.addEventListener("click", onTest);
</script>
</body>
</html>
As you can see from the javascript code, after clicking the button, the code sends a GET, this is just to retreive the antiforgery token. The GET is followed by a post. The CSRF-TOKEN is retreived from the cookies and included in the request headers. Note if trying this code out for yourself you will need to set your own baseUrl in the javascript code and also set your own url in the UseCors method in the Configure of Startup.

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

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 ConfigureServices Authentication based on routes in ASP.NET Core 2.0

In ASP.NET Core 1.x I could use authentication methods in Configure but now in ASP.NET Core 2.0 I have to set everything in ConfigureServices and can't configure it in Configure method. For example
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddCookie()
.AddXX();
}
and then in
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
....
app.UseAuthentication();
}
in the past, I could use something like
app.UseOpenIdConnectAuthentication();
and I can't configure it anymore like this.
so how I can use something like this now in ASP.NET Core 2.0?
app.Map(new PathString("/MyPath"), i => i.UseMyAuthMethod());
In 2.0, the best option to do per-route authentication is to use a custom IAuthenticationSchemeProvider:
public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
public CustomAuthenticationSchemeProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<AuthenticationOptions> options)
: base(options)
{
this.httpContextAccessor = httpContextAccessor;
}
private async Task<AuthenticationScheme> GetRequestSchemeAsync()
{
var request = httpContextAccessor.HttpContext?.Request;
if (request == null)
{
throw new ArgumentNullException("The HTTP request cannot be retrieved.");
}
// For API requests, use authentication tokens.
if (request.Path.StartsWithSegments("/api"))
{
return await GetSchemeAsync(OAuthValidationDefaults.AuthenticationScheme);
}
// For the other requests, return null to let the base methods
// decide what's the best scheme based on the default schemes
// configured in the global authentication options.
return null;
}
public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultAuthenticateSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultChallengeSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultForbidSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultForbidSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultSignInSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignInSchemeAsync();
public override async Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignOutSchemeAsync();
}
Don't forget to register it in the DI container (ideally, as a singleton):
// IHttpContextAccessor is not registered by default
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>();
The Microsoft docs say what to do if you want to use multiple authentication schemes in ASP.NET Core 2+:
The following example enables dynamic selection of schemes on a per
request basis. That is, how to mix cookies and API authentication:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// For example, can foward any requests that start with /api
// to the api scheme.
options.ForwardDefaultSelector = ctx =>
ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
})
.AddYourApiAuth("Api");
}
Example:
I had to implement a mixed-authentication solution in which I needed Cookie authentication for some requests and Token authentication for other requests. Here is what it looks like for me:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// if URL path starts with "/api" then use Bearer authentication instead
options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
{
o.TokenValidationParameters.ValidateIssuerSigningKey = true;
o.TokenValidationParameters.IssuerSigningKey = symmetricKey;
o.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
o.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
});
where the JWT Bearer authentication is implemented as described in this answer.
Tips:
One of the biggest 'gotchas' for me was this: Even though the Cookies Policy forwards requests with URLs that start with "/api" to the Bearer policy, the cookie-authenticated users can still access those URLs if you're using the [Authorize] annotation. If you want those URLs to only be accessed through Bearer authentication, you must use the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] annotation on the API Controllers/Actions.

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