ASP.Net Core SignalR authentication always responding with 403 - Forbidden - asp.net-core

Summary
I am trying to add security/authentication to my SignalR hubs, but no matter what I try the client requests keep getting a 403 - Forbidden responses (despite the requests successfully authenticating).
Setup
My project is based on Microsoft's SignalRChat example from:
https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio
Basically I have an ASP.Net Core web application with Razor Pages. The project is targeting .Net Core 3.1.
The client library being used is v3.1.0 of Microsoft's JavaScript client library.
I also referenced their authentication and authorization document for the security side:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1
The key difference is rather than using the JWT Bearer middleware, I made my own custom token authentication handler.
Code
chat.js:
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub", { accessTokenFactory: () => 'mytoken' })
.configureLogging(signalR.LogLevel.Debug)
.build();
//Disable send button until connection is established
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
using SignalRChat.Security;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Found other cases where CORS caused authentication issues, so
// making sure that everything is allowed.
services.AddCors(options =>
{
options.AddPolicy("AllowAny", policy =>
{
policy
.WithOrigins("http://localhost:44312/", "https://localhost:44312/")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services
.AddAuthentication()
.AddHubTokenAuthenticationScheme();
services.AddAuthorization(options =>
{
options.AddHubAuthorizationPolicy();
});
services.AddRazorPages();
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
ChatHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Security;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
[Authorize(HubRequirementDefaults.PolicyName)]
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
HubTokenAuthenticationHandler.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationHandler : AuthenticationHandler<HubTokenAuthenticationOptions>
{
public IServiceProvider ServiceProvider { get; set; }
public HubTokenAuthenticationHandler(
IOptionsMonitor<HubTokenAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IServiceProvider serviceProvider)
: base(options, logger, encoder, clock)
{
ServiceProvider = serviceProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
bool isValid = TryAuthenticate(out AuthenticationTicket ticket, out string message);
if (isValid) return Task.FromResult(AuthenticateResult.Success(ticket));
return Task.FromResult(AuthenticateResult.Fail(message));
}
private bool TryAuthenticate(out AuthenticationTicket ticket, out string message)
{
message = null;
ticket = null;
var token = GetToken();
if (string.IsNullOrEmpty(token))
{
message = "Token is missing";
return false;
}
bool tokenIsValid = token.Equals("mytoken");
if (!tokenIsValid)
{
message = $"Token is invalid: token={token}";
return false;
}
var claims = new[] { new Claim("token", token) };
var identity = new ClaimsIdentity(claims, nameof(HubTokenAuthenticationHandler));
ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
return true;
}
#region Get Token
private string GetToken()
{
string token = Request.Query["access_token"];
if (string.IsNullOrEmpty(token))
{
token = GetTokenFromHeader();
}
return token;
}
private string GetTokenFromHeader()
{
string token = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(token)) return null;
// The Authorization header value should be in the format "Bearer [token_value]"
string[] authorizationParts = token.Split(new char[] { ' ' });
if (authorizationParts == null || authorizationParts.Length < 2) return token;
return authorizationParts[1];
}
#endregion
}
}
HubTokenAuthenticationOptions.cs
using Microsoft.AspNetCore.Authentication;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationOptions : AuthenticationSchemeOptions { }
}
HubTokenAuthenticationDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
HubRequirement.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
HubRequirementDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
The Results
On the client side, I see the following errors in the browser's developer console:
POST https://localhost:44312/chatHub/negotiate?negotiateVersion=1 403
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
On the server side, all I see is:
SignalRChat.Security.HubTokenAuthenticationHandler: Debug: AuthenticationScheme: HubTokenAuthentication was successfully authenticated.
SignalRChat.Security.HubTokenAuthenticationHandler: Information:
AuthenticationScheme: HubTokenAuthentication was forbidden.
Next Steps
I did see that others had issues with CORS preventing them from security from working, but I believe it usually said that explicitly in the client side errors. Despite that, I added the CORS policies in Startup.cs that I believe should have circumvented that.
I also experimented around with changing the order of service configurations in Startup, but nothing seemed to help.
If I remove the Authorize attribute (i.e. have an unauthenticated hub) everything works fine.
Finally, I found the server side messages to be very interesting in that the authentication succeeded, yet the request was still forbidden.
I'm not really sure where to go from here. Any insights would be most appreciated.
Update
I have been able to debug this a little bit.
By loading system symbols and moving up the call stack, I found my way to Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator:
As can be seen, the authentication succeeded but apparently the authorization did not. Looking at the requirements, there are two: a DenyAnonymousAuthorizationRequirement and my HubRequirement (which automatically succeeds).
Because the debugger never hit my breakpoint in my HubRequirement class, I am left to assume that the DenyAnonymousAuthorizationRequirement is what is failing. Interesting, because based on the code listing on github (https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs) I should be meeting all the requirements:
There is a User defined on the context, the user has an identity, and there are no identities that are unauthenticated.
I have to be missing something, because this isn't adding up.

Turns out the failure was actually happening in my HubRequirement class, and not DenyAnonymousAuthorizationRequirement.
While my HubRequirement class implemented HandleRequirementAsync(), it did not implement HandleAsync(), which is what happened to be what was called instead.
If I update my HubRequirement class to the following, everything works as expected:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
public override Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.PendingRequirements)
{
// TODO: Validate each requirement
}
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(this);
return Task.CompletedTask;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

Thank you, saved me a lot of debugging hours!
Looks like the problem is that HandleAsync is also being called with a RouteEndpoint resource for the signalr root and negotiation urls, a case the base class does not handle and since no authorization handler signals success it fails.
public override async Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.Resource is HubInvocationContext)
{
foreach (var req in context.Requirements.OfType<RealtimeHubSecurityAuthorizationHandler>())
{
await HandleRequirementAsync(context, req, (HubInvocationContext)context.Resource);
}
} else if (context.Resource is Microsoft.AspNetCore.Routing.RouteEndpoint) {
//allow signalr root and negotiation url
context.Succeed(this);
}
}
(posted as answer since comment length is limited, sorry)

Related

Differ IOutputFormatter per endpoint in ASP.NET Core 6

I have a legacy ASP.NET Web API 2 app which must be ported to ASP.NET Core 6 and it has the following behaviour:
Some controllers return responses in Pascal-case Json
Some controllers return responses in camel-case Json
All controllers have the same authentication/authorization, but they return different objects using different serializers for 401/403 cases.
In ASP.NET Web API 2 it was easily solved with IControllerConfiguration (to set the formatter for a controller), AuthorizeAttribute (to throw exceptions for 401/403), ExceptionFilterAttribute to set 401/403 status code and response which will be serialized using correct formatter.
In ASP.NET Core, it seems that IOutputFormatter collection is global for all controllers and it is not available during UseAuthentication + UseAuthorization pipeline where it terminates in case of failure.
Best I could come up with is to always "succeed" in authentication / authorization with some failing flag in claims and add IActionFilter as first filter checking those flags, but it looks very hacky.
Is there some better approach?
Update1:
Implementing different output formatters for IActionResult from controller or IFilter (including IExceptionFilter) is not very difficult.
What I want is to be able to either set IActionResult or use IOutputFormatter related to Action identified by UseRouting for Authentication/Authorization error or IAuthorizationHandler, but looks like all those auth steps are invoked before either ActionContext or IOutputFormatter is invoked.
So 2 approaches I see now:
hack auth code to "always pass" and handle HttpContext.Items["MyRealAuthResult"] object in IActionFilter
expose V1OutputFormatter/V2OutputFormatter in a static field and duplicate selection logic in HandleChallengeAsync/HandleForbiddenAsync based on to what controller/action it was routed from UseRouting step.
Here is sample app that uses auth and has 2 endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
builder.Services.AddAuthentication(options =>
{
options.AddScheme<DefAuthHandler>("defscheme", "defscheme");
});
builder.Services.AddAuthorization(options =>
options.DefaultPolicy = new AuthorizationPolicyBuilder("defscheme")
.RequireAssertion(context =>
// false here should result in Pascal case POCO for WeatherForecastV1Controller
// and camel case POCO for WeatherForecastV2Controller
context.User.Identities.Any(c => c.AuthenticationType == "secretheader"))
.Build())
.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultHandler>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
public class AuthorizationResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultHandler()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
// Can't set ActionContext.Response here or use IOutputFormatter
await _handler.HandleAsync(next, context, policy, authorizeResult);
}
}
public class DefAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DefAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<ClaimsIdentity>();
if (Request.Headers.ContainsKey("secretheader")) claims.Add(new ClaimsIdentity("secretheader"));
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claims), "defscheme"));
}
}
public class MvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ArrayPool<char> arrayPool;
private readonly MvcNewtonsoftJsonOptions mvcNewtonsoftJsonOptions;
public MvcOptionsSetup(ArrayPool<char> arrayPool, IOptions<MvcNewtonsoftJsonOptions> mvcNewtonsoftJsonOptions)
{
this.arrayPool = arrayPool;
this.mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions.Value;
}
public void Configure(MvcOptions options)
{
options.OutputFormatters.Insert(0, new V1OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
options.OutputFormatters.Insert(0, new V2OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
}
}
public class V1OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V1OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV1";
}
}
public class V2OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V2OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV2";
}
}
[ApiController]
[Authorize]
[Route("v1/weatherforecast")]
public class WeatherForecastV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be Pascal case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
[ApiController]
[Authorize]
[Route("v2/weatherforecast")]
public class WeatherForecastV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be camel case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
If there is no way to configure controllers independently, then you could use some middleware to convert output from selected controllers that meet a path-based predicate.
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapWhen(ctx => ctx.Request.Path.Containes("v2/"), cfg =>
{
app.UseMiddleware<JsonCapitalizer>();
});
app.Run();
And then create a JsonCapitalizer class to convert output from any path that contains "v2/". Note, this middleware will not run if the predicate in MapWhen is not satisfied.
public class JsonCapitalizer
{
readonly RequestDelegate _nextRequestDelegate;
public RequestLoggingMiddleware(
RequestDelegate nextRequestDelegate)
{
_nextRequestDelegate = nextRequestDelegate;
}
public async Task Invoke(HttpContext httpContext)
{
await _nextRequestDelegate(httpContext);
// Get the httpContext.Response
// Capitalize it
// Rewrite the response
}
}
There may be better ways, but that's the first that comes to mind.
The following link will help with manipulation of the response body:
https://itecnote.com/tecnote/c-how-to-read-asp-net-core-response-body/
I also faced such a problem in ASP Core 7 and ended up with writing an attribute.
So the attribute will be applied on each Action where the response type has to be converted. You can write many an attribute for camelcase response and another attribute for pascalcase. The attribute will look like below for CamelCase
public class CamelCaseAttribute : ActionFilterAttribute
{
private static readonly SystemTextJsonOutputFormatter formatter = new SystemTextJsonOutputFormatter(new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Formatters
.RemoveType<NewtonsoftJsonOutputFormatter>();
objectResult.Formatters.Add(formatter);
}
else
{
base.OnActionExecuted(context);
}
}
}
And on the Contoller Action you can use it like below
[CamelCase]
public async IAsyncEnumerable<ResponseResult<IReadOnlyList<VendorBalanceReportDto>>> VendorBalanceReport([FromQuery] Paginator paginator, [FromQuery] VendorBalanceReportFilter filter, [EnumeratorCancellation] CancellationToken token)
{
var response = _reportService.VendorBalanceReport(paginator, filter, token);
await foreach (var emailMessage in response)
{
yield return emailMessage;
}
}

Multiple authentication schemes in ASP.NET Core 5.0 WebAPI

I have a full set of (ASP.NET Core) web APIs developed in .NET 5.0 and implemented Cookies & OpenIdConnect authentication schemes.
After successful authentication (user id and password) with Azure AD, cookie is generated and stores user permissions etc.
Now, I would like to expose the same set of APIs to a third party consumer using API Key based authentication (via api-key in the request headers).
I have developed a custom authentication handler as below.
using Microsoft.AspNetCore.Authentication;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
}
namespace Management.Deployment.Securities.Authentication
{
public static class ApiKeyAuthenticationDefaults
{
public static readonly string AuthenticationScheme = "ApiKey";
public static readonly string DisplayName = "ApiKey Authentication Scheme";
}
}
ApiKeyAuthenticationHandler is defined as below, straight forward, if the request headers contain the valid api key then add permissions claim (assigned to the api key) and mark the authentication as success else fail.
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Management.Securities.Authorization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
private const string APIKEY_NAME = "x-api-key";
private const string APIKEY_VALUE = "sdflasuowerposaddfsadf1121234kjdsflkj";
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string extractedApiKey = Request.Headers[APIKEY_NAME];
if (!APIKEY_VALUE.Equals(extractedApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("Unauthorized client."));
}
var claims = new[]
{
new Claim("Permissions", "23")
};
var claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
}
I have also defined ApiKeyAuthenticationExtensions as below.
using Microsoft.AspNetCore.Authentication;
using Management.Deployment.Securities.Authentication;
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, x => { });
}
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationSchemeOptions> configureOptions)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, configureOptions);
}
}
}
Skimmed version of ConfigureServices() in Startup.cs is here. Please note I have used ForwardDefaultSelector.
public void ConfigureServices(IServiceCollection services)
{
IAuthCookieValidate cookieEvent = new AuthCookieValidate();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = ".Mgnt.AspNetCore.Cookies";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.FromResult(0);
},
OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.FromResult(0);
},
OnValidatePrincipal = cookieEvent.ValidateAsync
};
options.ForwardDefaultSelector = context =>
{
return context.Request.Headers.ContainsKey(ApiConstants.APIKEY_NAME) ? ApiKeyAuthenticationDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
};
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => Configuration.Bind(OpenIdConnectDefaults.AuthenticationScheme, options))
.AddApiKey();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
}
The Configure method is as below.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHsts();
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
When I send the correct apikey in the request headers, the custom handler is returning success as authentication result and the request is processed further.
But if an incorrect api key is passed, it is not returning the authentication failure message - "Unauthorized client.". Rather, the request is processed further and sending the attached response.
What changes to be made to resolve this issue so the api returns the authentication failure message - "Unauthorized client." and stops further processing of the request?
if you plan to use apikeys, then you are on your own and there is (as far as I know) no built in direct support for API-keys. There is however built in support for JWT based access tokens and I would recommend that you use that as well for external third parties who wants to access your api. Perhaps using client credentials flow.
For some help, see http://codingsonata.com/secure-asp-net-core-web-api-using-api-key-authentication/
I also think you should configure and let the authorization handler be responsible for deciding who can access the services.
see Policy-based authorization in ASP.NET Core

How to have a Self Hosting signalR server running as as NetCore Console App

I would like to create a SignalR Self hosting Server within a console app using .NetCore.
I am completely new to web development and .Net Core but would like to use SignalR as a real-time web based protocol. No web page is required, and so I would like to have a console app.
I have successfully tested the .Net Framework example below and would like to replicate this using .Net Core 3.1, so that it can run on Linux. However I cannot find any suitable examples.
using System;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Hosting;
using Owin;
using Microsoft.Owin.Cors;
namespace SignalRSelfHost
{
class Program
{
static void Main(string[] args)
{
// This will *ONLY* bind to localhost, if you want to bind to all addresses
// use http://*:8080 to bind to all addresses.
// See http://msdn.microsoft.com/library/system.net.httplistener.aspx
// for more information.
string url = "http://localhost:8088";
using (WebApp.Start<Startup>(url))
{
Console.WriteLine("Server running on {0}", url);
Console.ReadLine();
}
}
}
class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR();
}
}
public class MyHub : Hub
{
public void Send(string name, string message)
{
Clients.All.addMessage(name, message);
Clients.All.addMessage(name, "World");
}
}
}
In an attempt to use Owin to create a server console app I have the following code and this compiles, however complains about no server service being registered when I run the program. Could someone please advise what to add to have a web server without web page? The example I copied specified UseKestrel() but I think this is for a web page, so I think I need something else.
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace OwinConsole
{
public class Startup
{
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app)
{
app.UseOwin(pipeline =>
{
pipeline(next => OwinHello);
});
}
public Task OwinHello(IDictionary<string, object> environment)
{
string responseText = "Hello World via OWIN";
byte[] responseBytes = Encoding.UTF8.GetBytes(responseText);
// OWIN Environment Keys: http://owin.org/spec/spec/owin-1.0.0.html
var responseStream = (Stream)environment["owin.ResponseBody"];
var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];
responseHeaders["Content-Length"] = new string[] { responseBytes.Length.ToString(CultureInfo.InvariantCulture) };
responseHeaders["Content-Type"] = new string[] { "text/plain" };
return responseStream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
}
}
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace OwinConsole
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
Thanks.
For those who want to achive this in .NET 6:
To create a simple server as a console application, you have to create a new empty ASP.NET Core project. In .NET 6 you don't need the 'startup.cs' anymore. You just need to change a few things in 'Program.cs' to configure SignalR.
Program.cs
var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
//app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();
Add a Hub
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
Console.WriteLine("Received message, sending back echo");
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Client (console application)
For example:
using Microsoft.AspNetCore.SignalR.Client;
namespace Client
{
public class Program
{
private static HubConnection _connection;
public static async Task Main(string[] args)
{
_connection = new HubConnectionBuilder()
.WithUrl("https://localhost:7116/chatHub")
.Build();
_connection.Closed += async (error) =>
{
await Task.Delay(new Random().Next(0, 5) * 1000);
await _connection.StartAsync();
};
await ConnectAsync();
bool stop = false;
while (!stop)
{
Console.WriteLine("Press any key to send message to server and receive echo");
Console.ReadKey();
Send("testuser", "msg");
Console.WriteLine("Press q to quit or anything else to resume");
var key = Console.ReadLine();
if (key == "q") stop = true;
}
}
private static async Task ConnectAsync()
{
_connection.On<string, string>("ReceiveMessage", (user, message) =>
{
Console.WriteLine("Received message");
Console.WriteLine($"user: {user}");
Console.WriteLine($"message: {message}");
});
try
{
await _connection.StartAsync();
}
catch(Exception e)
{
Console.WriteLine("Exception: {0}", e);
}
}
private static async void Send(string user, string msg)
{
try
{
await _connection.InvokeAsync("SendMessage", user, msg);
}
catch (Exception e)
{
Console.WriteLine($"Exception: {e}");
}
}
}
}
The client will connect to the server, after that in a loop you can send a message to the server by pressing any key and the server will send you the same message back.
In 'launchSettings.json' (Server) you can find the applicaitonUrl
As mentioned by Noah, my solution was based on
[https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio]
But instead was built as a console app referencing Microsoft.AspNetCore.App (2.2.8).
ChatHub
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRServer
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
Startup
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace SignalRServer
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//services.AddRazorPages();
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
//endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
Program
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace SignalRServer
{
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>();
webBuilder.UseUrls("http://localhost:2803");
});
}
}

Is there a way to handle asp.net core odata errors

Is there a way to handle asp.net core odata errors?
I have a model class DimDateAvailable with one property, a primary key of int DateId, and I make a call like /data/DimDateAvailable?$select=test.
Other calls work as expected and return what I'm after - this is a deliberate call to generate a fault, and it fails because there is no property named test on the model. The response comes back as expected, like so: {"error":{"code":"","message":"The query specified in the URI is not valid. Could not find a property named 'test' on type 'DimDateAvailable'... followed by a stack trace.
This response is fine when env.IsDevelopment() is true but I don't want to expose the stack trace when not in development.
I've looked at wrapping the code in the controllers' get method in a try-catch, but I think there's an action filter running over the results so it never gets called. On the other hand, I can't see where to inject any middleware and/or add any filters to catch errors. I suspect there might be a way to override an output formatter to achieve what I want but I can't see how.
Here's what I have at the moment:
In Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<DimDateAvailable>("DimDateAvailable");
return builder.GetEdmModel();
}
In TelemetryDbContext.cs:
public virtual DbSet<DimDateAvailable> DimDateAvailable { get; set; }
In DimDateAvailable.cs
public class DimDateAvailable
{
[Key]
public int DateId { get; set; }
}
My controller:
public class DimDateAvailableController : ODataController
{
private readonly TelemetryDbContext data;
public DimDateAvailableController(TelemetryDbContext data)
{
this.data = data;
}
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IActionResult Get()
{
return Ok(this.data.DimDateAvailable.AsQueryable());
}
}
This is in an asp.net core 2 web app with the Microsoft.AspNetCoreOData v7.0.1 and EntityFramework 6.2.0 packages.
Investigating Ihar's suggestion lead me down the rabbit hole, and I ended up inserting an ODataOutputFormatter into the MVC options to intercept ODataPayloadKind.Error responses and reformat them.
It was interesting to see that context.Features held an instance of IExceptionHandlerFeature in app.UseExceptionHandler() but not in the ODataOutputFormatter. That lack was pretty much what prompted me to pose this question in the first place, but was solved by translating the context.Object in the ODataOutputFormatter which is something I saw done in the OData source as well. I don't know if the changes below are good practice in asp.net core or when using the AspNetCoreOData package, but they do what I want for now.
Changes to Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new CustomODataOutputFormatter(this.Environment.IsDevelopment()));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Added this to catch errors in my own code and return them to the client as ODataErrors
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
if (error?.Error != null)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var response = error.Error.CreateODataError(!env.IsDevelopment());
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
// when no error, do next.
else await next();
});
});
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
New classes CustomODataOutputFormatter.cs and CommonExtensions.cs
public class CustomODataOutputFormatter : ODataOutputFormatter
{
private readonly JsonSerializer serializer;
private readonly bool isDevelopment;
public CustomODataOutputFormatter(bool isDevelopment)
: base(new[] { ODataPayloadKind.Error })
{
this.serializer = new JsonSerializer { ContractResolver = new CamelCasePropertyNamesContractResolver() };
this.isDevelopment = isDevelopment;
this.SupportedMediaTypes.Add("application/json");
this.SupportedEncodings.Add(new UTF8Encoding());
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (!(context.Object is SerializableError serializableError))
{
return base.WriteResponseBodyAsync(context, selectedEncoding);
}
var error = serializableError.CreateODataError(this.isDevelopment);
using (var writer = new StreamWriter(context.HttpContext.Response.Body))
{
this.serializer.Serialize(writer, error);
return writer.FlushAsync();
}
}
}
public static class CommonExtensions
{
public const string DefaultODataErrorMessage = "A server error occurred.";
public static ODataError CreateODataError(this SerializableError serializableError, bool isDevelopment)
{
// ReSharper disable once InvokeAsExtensionMethod
var convertedError = SerializableErrorExtensions.CreateODataError(serializableError);
var error = new ODataError();
if (isDevelopment)
{
error = convertedError;
}
else
{
// Sanitise the exposed data when in release mode.
// We do not want to give the public access to stack traces, etc!
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = convertedError.Message } };
}
return error;
}
public static ODataError CreateODataError(this Exception ex, bool isDevelopment)
{
var error = new ODataError();
if (isDevelopment)
{
error.Message = ex.Message;
error.InnerError = new ODataInnerError(ex);
}
else
{
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = ex.Message } };
}
return error;
}
}
Changes to the controller:
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IQueryable<DimDateAvailable> Get()
{
return this.data.DimDateAvailable.AsQueryable();
}
If you want a customization of responses, including customization of error responses try to use ODataQueryOptions instead of using
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
Check some samples at https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#invoking-query-options-directly
It would allow you to cache validation errors and build custom response.
I have had this issue in the past and the only one way I got this working without having to write a middleware was like:
Try this:
catch (ODataException ex)
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;//This line is important, if not it will return 500 Internal Server Error.
return BadRequest(ex.Message);//Just respond back the actual error which is 100% correct.
}
Then the error will look like:
{
"#odata.context": "http://yourendpoint.com$metadata#Edm.String",
"value": "The property 'test' cannot be used in the $select query option."
}
Hope this helps.
Thanks

Populate custom claim from SQL with Windows Authenticated app in .Net Core

Scenario - .Net Core Intranet Application within Active Directory using SQL Server to manage application specific permissions and extended user identity.
Success to date - User is authenticated and windows claims are available (Name and Groups). Identity.Name can be used to return a domain user model from the database with the extended properties.
Issue and Question - I am trying to then populate one custom claim property "Id" and have that globally available via the ClaimsPrincipal. I have looked into ClaimsTransformation without much success to date. In other articles I have read that you MUST add claims prior to Sign In but can that really be true? That would mean total reliance on AD to fulfil all claims, is that really the case?
Below is my simple code at this point in the HomeController. I am hitting the database and then trying to populate the ClaimsPrincipal but then return the domain user model. I think this could be where my problem lies but I am new to Authorisation in .net and struggling to get my head around claims.
Many thanks for all help received
Current Code:
public IActionResult Index()
{
var user = GetExtendedUserDetails();
User.Claims.ToList();
return View(user);
}
private Models.User GetExtendedUserDetails()
{
var user = _context.User.SingleOrDefault(m => m.Username == User.Identity.Name.Remove(0, 6));
var claims = new List<Claim>();
claims.Add(new Claim("Id", Convert.ToString(user.Id), ClaimValueTypes.String));
var userIdentity = new ClaimsIdentity("Intranet");
userIdentity.AddClaims(claims);
var userPrincipal = new ClaimsPrincipal(userIdentity);
return user;
}
UPDATE:
I have registered ClaimsTransformation
app.UseClaimsTransformation(o => new ClaimsTransformer().TransformAsync(o));
and built ClaimsTransformer as below in line with this github query
https://github.com/aspnet/Security/issues/863
public class ClaimsTransformer : IClaimsTransformer
{
private readonly TimesheetContext _context;
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
System.Security.Principal.WindowsIdentity windowsIdentity = null;
foreach (var i in context.Principal.Identities)
{
//windows token
if (i.GetType() == typeof(System.Security.Principal.WindowsIdentity))
{
windowsIdentity = (System.Security.Principal.WindowsIdentity)i;
}
}
if (windowsIdentity != null)
{
//find user in database
var username = windowsIdentity.Name.Remove(0, 6);
var appUser = _context.User.FirstOrDefaultAsync(m => m.Username == username);
if (appUser != null)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim("Id", Convert.ToString(appUser.Id)));
/*//add all claims from security profile
foreach (var p in appUser.Id)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(p.Permission, "true"));
}*/
}
}
return await System.Threading.Tasks.Task.FromResult(context.Principal);
}
}
But am getting NullReferenceException: Object reference not set to an instance of an object error despite having returned the domain model previously.
WITH STARTUP.CS
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Birch.Intranet.Models;
using Microsoft.EntityFrameworkCore;
namespace Birch.Intranet
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization();
// Add framework services.
services.AddMvc();
// Add database
var connection = #"Data Source=****;Initial Catalog=Timesheet;Integrated Security=True;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
services.AddDbContext<TimesheetContext>(options => options.UseSqlServer(connection));
// Add session
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(60);
options.CookieName = ".Intranet";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseClaimsTransformation(o => new ClaimsTransformer().TransformAsync(o));
app.UseSession();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
You need to use IClaimsTransformer with dependency injection.
app.UseClaimsTransformation(async (context) =>
{
IClaimsTransformer transformer = context.Context.RequestServices.GetRequiredService<IClaimsTransformer>();
return await transformer.TransformAsync(context);
});
// Register
services.AddScoped<IClaimsTransformer, ClaimsTransformer>();
And need to inject DbContext in ClaimsTransformer
public class ClaimsTransformer : IClaimsTransformer
{
private readonly TimesheetContext _context;
public ClaimsTransformer(TimesheetContext dbContext)
{
_context = dbContext;
}
// ....
}