Auth0, .NETCoreApp 3.1 and SignalR: obtaining decoded JWT - c#

I had developed a chat app that uses socket.io for communication between client and server, but I ran into serious problems when trying to scale across more than one node. So I decided to give SignalR a go...
I am using Auth0 for my authentication and had previously developed a library to allow Auth0 to integrate with socket.io. I am now trying to understand how I can integrate Auth0 with SignalR and my React front end.
It is going well, I have managed to get the app authenticating over the socket/HTTP handshake, but I am struggling to understand how to obtain the sub and other information from the decoded JWT, since this sub uniquely identifies my user to the application in general, but to the SignalR hubs in particular.
The difficulty is that the authentication is happening over HTTP, and so the hub is unavailable at that point (or is it? Told you I didn't know what I was talking about...)
I am completely new to ASP.NET Core, and SignalR, so I think I've done quite well! But can someone help me out with obtaining the JWT within my hubs? Here is my startup.cs:
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.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder =>
{
builder
.WithOrigins("http://localhost:3010", "http://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
var domain = $"https://{Configuration["Auth0:Domain"]}/";
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = domain;
options.Audience = Configuration["Auth0:Audience"];
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/chathub")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("read:messages",
policy => policy.Requirements.Add(new HasScopeRequirement("read:messages", domain)));
});
services.AddControllers();
services.AddSignalR();
// Register the scope authorization handler
services.AddSingleton<IAuthorizationHandler, HasScopeHandler>();
}
// 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.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("AllowSpecificOrigin");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<ChatHub>("/chathub");
});
}
}
and here is my hub, such as it is:
namespace WebAPIApplication.Hubs
{
[Authorize]
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// How do I access the JWT sub here?
await Clients.All.SendAsync("ReceiveMessage", "Some message");
}
}
}
Please help!

Related

No service for type ' Microsoft.AspNetCore.Components.WebAssembly.Authentication.BaseAddressAuthorizationMessageHandler' Error

I am currently working on a project that consists of sub-projects such as WebApp, API, and Client class library. (The project structure is shown below).
Project Solution Structure
Although the project is a web-based project, it uses windows Identity as authentication identity since it is an internal application. I implemented the authorization policy of the WebApp project without any problems by following the steps in the implementation_link.
Now I can control access using DataAnnotation in WebApp (ex. [Authorize(Roles = "Admin"]). If I add Authorization control on the API side, WebApp cannot access this API. This is because of HttpContext.User is null. I found the solution to this problem solution_link. I adapted this solution to the project as below:
ServiceCollectionExtensions.cs in WebApp project:
public static IServiceCollection AddAuraServices(this IServiceCollection serviceCollection, IConfiguration configuration)
{
serviceCollection.AddTransient<IModelDatabaseNamesProvider, StandardCasingModelDatabasesNamesProvider>();
serviceCollection.Configure<RouteOptions>(routeOptions =>
{
routeOptions.ConstraintMap.Add(ModelDatabasesNameConstraint.Name, typeof(ModelDatabasesNameConstraint));
});
serviceCollection.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
serviceCollection.AddScoped<IModelMetadataProvider>(serviceProvider =>
{
var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
var modelName = httpContext.Request.RouteValues["model-name"].ToString();
return new ModelMetadataProvider(modelName);
});
DateOnlyTypeConverter.AddAttributeToType();
serviceCollection.AddHttpClient<UploadRulesClient>("ServerAPI", (httpClient) =>
{
httpClient.BaseAddress = new Uri(configuration["AuraApiClient:BaseAddress"]);
}).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
serviceCollection.AddHttpClient<ScenarioZipFilesClient>("ServerAPI",(httpClient) =>
{
httpClient.BaseAddress = new Uri(configuration["AuraApiClient:BaseAddress"]);
}).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
serviceCollection.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("ServerAPI"));
var jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
ClientJsonResponse.Configure(jsonSerializerOptions);
serviceCollection.AddSingleton(jsonSerializerOptions);
serviceCollection.AddAuraDropzoneConfig(configuration);
return serviceCollection;
}
Startup.cs of WebApp:
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.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
services.AddAuthorization();
services.AddControllersWithViews();
//services.AddRazorPages();
services.AddAuraServices(Configuration);
}
// 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.MapControllerRoute(name: "model-database", pattern: "{model-name:modeldatabasename}/{controller=ZipFiles}/{action=Index}/{id?}");
endpoints.MapControllerRoute(name: "default", pattern: "", new { controller = "Home", action = "Index" });
//endpoints.MapRazorPages();
});
}
}
But this time I am getting No service for Type Error. How can I solve this problem? Where do you think I am going wrong? Thanks
Edit:
As you can see BaseAddressAuthorizationMessageHandler is in namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication. It is supposed to be used with Blazor WebAssembly apps and it attaches the access token to the authentication header of HttpClient requests. BaseAddressAuthorizationMessageHandler depends on other services like IAccessTokenProvider which is responsible to return the access token. For example in web assembly IAccessTokenProvider default implementation retrieves the access token from browser session storage.
If you want to attach access tokens to your http requests your should probably implement your own DelegatingHandler instead of BaseAddressAuthorizationMessageHandler.
Old answer:
You have to register BaseAddressAuthorizationMessageHandler:
serviceCollection.AddTransient<BaseAddressAuthorizationMessageHandler>();

Azure AD Authentication seems to be skipped in my ASP.Net Core 2.2 application

I have an empty ASP.Net Core application and I'd like to have Azure AD authentication invoked. However my "milldeware" seems always ignore authentication. Please help me to figure out the root cause. Thank you.
My appliaction is in .Net Core 2.2
Below is my startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.AddAuthorization(auth =>
{
auth.AddPolicy("AzureAD", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme‌​)
.RequireAuthenticatedUser().Build());
});
services.AddMvc();
services.AddRouting();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseRouter(routes =>
{
routes.MapGet(string.Empty, HomeMiddleware);
routes.MapGet("test", TestMiddleware);
});
}
Below are some middlewares I'd like to routes.
[Authorize]
private Task HomeMiddleware(HttpContext context)
{
return context.Response.WriteAsync($"control, User: {context.User.Identity.Name}");
}
[Authorize]
private Task TestMiddleware(HttpContext context)
{
return Task.Run(() =>
{
var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8);
writer.Write("test");
writer.Flush();
writer.Write("another test");
writer.Flush();
});
}
}
Seems [Authorize] doesn't work for my 'middleware'. the context.User.Identity.Name returns nothing to me and it doesn't redirect me to AzureAD authentication page.
Update: net core 2.2 My bad.
I'm not sure the authorizeattribute works out of controllers. But I'd give it a try specifying the name of the policy like:
[Authorize("AzureAD")]
If that works but you don't want to specify the policy every time, I've seen in this link https://learn.microsoft.com/es-es/aspnet/core/security/authorization/secure-data?view=aspnetcore-2.2 there should be a FallbackPolicy property like the DefaultPolicy in 3.1:
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
This is for 3.1.
First, you wan't to setup a default policy not to create just a policy. For that use
auth.AddDefaultPolicy(builder => builder
.AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme‌​)
.RequireAuthenticatedUser().Build());
Second, you want to add the Authorization middleware, not only the authentication one.
app.UseAuthentication();
app.UseAuthorization();
app.UseRouter(routes =>
...
Without this middleware Authorize attributes have no effect afaik
You can also leave the policy as it is and
A) use it on the app.UseAuthorization() call, I belive there's a overload to choose the default policy name there.
B) leave it being a named not default policy and use [Authorize("<policyName>")] on your endpoints.

Hangfire Dashboard only works with local requests - even with implemented filter

I'm using Hangfire to schedule some jobs, but when deployed to production the Dashboard does not work.
I've already implemented the authorization process (https://docs.hangfire.io/en/latest/configuration/using-dashboard.html#configuring-authorization), but it didn't help. Here's my relevant code:
HangfireDashboardAuthorizationFilter.cs
public class HangfireDashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var hasClaim = context.GetHttpContext().User.Claims
.Any(claim => claim.Type == "customClaimType" &&
claim.Value == "customClaimValue");
return hasClaim;
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services){
// Add Hangfire services.
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage()
);
// Add the processing server as IHostedService
services.AddHangfireServer();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery){
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(routes =>
{
routes.MapAreaControllerRoute(
name: "areas",
areaName: "areas",
pattern: "{area:exists}/{controller=Default}/{action=Index}/{id?}");
routes.MapDefaultControllerRoute();
routes.MapRazorPages();
});
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new []{ new HangfireDashboardAuthorizationFilter() },
StatsPollingInterval = 30000,
});
}
While debugging, everything works as expected. When the application is deployed to production, the filter works for the purpose of denying requests if an unauthenticated user (or an user that does not have the necessary claims) tries to access the "/hangfire" endpoint, and an expected 401 response is returned. However, when the user has the necessary claims, a 200 response code is returned with no content.
I've already been googling for a solution but did not find anything, any help will be appreciated. Thanks.

AuthenticateResult.Principal is null when trying to authenticate with Google

I'm trying to allow users to log in with their Google account on my ASP.NET Core Blazor app. Whenever I go to my app/login/google-login, everything works as expected. I get redirected to google's login page and I get to choose an account to log in with. After choosing my account, it takes a few seconds to load and then visual studio 2019 tells me this:
System.NullReferenceException: 'Object reference not set to an instance of an object.' Microsoft.AspNetCore.Authentication.AuthenticateResult.Principal.get returned null.
at this block of code:
var claims = response.Principal.Identities.FirstOrDefault().Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
Some debugging has revealed the following:
{"succeeded":false,"ticket":null,"principal":null,"properties":null,"failure":null,"none":true}
This is the response I get from Google's API formatted as JSON. Basically it tells me that the principal is null, which I could have guessed, but the rest is also null. What's going on here? Could this simply be an issue with my scopes on Google's end? I have reasons to believe this isn't the problem though since my app should be able to work with any API response without crashing, right?
Here's my LoginController.cs class:
[AllowAnonymous, Route("login")]
public class LoginController : Controller
{
[Route("google-login")]
public IActionResult GoogleLogin()
{
var properties = new AuthenticationProperties { RedirectUri = Url.Action("GoogleResponse") };
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[Route("google-response")]
public async Task<IActionResult> GoogleResponse()
{
var response = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var claims = response.Principal.Identities.FirstOrDefault().Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
return Json(claims);
}
}
Here's my Startup.cs:
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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
//services.AddSingleton<WeatherForecastService>();
services.AddDbContext<Context>(options => options.UseSqlServer(Configuration.GetConnectionString("Context")));
services.AddIdentity<User, Role>().AddEntityFrameworkStores<Context>();
services.AddHttpContextAccessor();
services.AddScoped<IReservationService, ReservationService>();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login/google-login";
})
.AddGoogle(options =>
{
options.ClientId = Configuration["Google:ClientID"];
options.ClientSecret = Configuration["Google:ClientSecret"];
});
}
// 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.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
If you need any additional info or code, I'll be happy to provide that ASAP.
This line, that is arguably a fairly big part of my project, was preventing communication with Google for who knows what reason.
services.AddIdentity<User, Role>().AddEntityFrameworkStores<Context>();
I now basically just went on without UserManager and RoleManager and just wrote manual methods for accessing AspNetUsers etc.
Probably not a real solution but it is what it is.

Blazor Web Assembly Hosted - Bearer error="invalid_token", error_description="The signature key was not found"

Having upgraded from 3.1 to 5.0, I cannot seem to get past this HttpRequestException error when trying to fetch data from protected api controllers (those not marked [Authorize] are fine).
Although the client app tells me I am authenticated (I have 'hello, user#example.com' up top right for example), I can't seem to be get authenticated with the server. User.Identity just looks like this for example:
I have a few typed HttpClients, the authenticating one looking like so
public class ApplicationClient
{
private readonly HttpClient _httpClient;
public ApplicationClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Applicant> GetApplicant() => await _httpClient.GetFromJsonAsync<Applicant>("applicant");
}}
With ApplicationClient being register in Programme.cs on client side as:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddHttpClient<ApplicationClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Jcc.ServerAPI"));
builder.Services.AddScoped<ApplicationState>();
builder.Services.AddBlazoredModal();
builder.Services.AddApiAuthorization();
await builder.Build().RunAsync();
}
And finally a controller,
[Route("[controller]")]
[ApiController]
[Authorize]
public class MyController
{
public MyController(...) : base(...)
{
...
}
[HttpGet]
public async Task<ActionResult<Applicant>> GetCurrentApplicant()
{
...
}
In Startup.cs on Server app (having chopped and changed and tried about every combination of the below plus others) the pertinent bits of ConfigureServices() are:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<JccContext>(options =>
options.UseNpgsql(
Configuration.GetConnectionString("DbContext")));
services.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<DbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, DbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto |
ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
while Configure() goes:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
I just don't what's changed since it worked previously. Identity Server seems to generate the token fine:
but I keep seeing this:
It would be nice to not have to revert back to 3.1 :)
I did try
services.AddIdentityServer(opt =>
opt.IssuerUri = "https://localhost:5001"
)
As per a post a few years back but no dice and I think it was a different issue.
If you regenerate the signing keys, then the keys in tokens already issued will be invalidated. For production you need to make sure the signing keys is persisted.
you can look at the kid claim in the JWT header of your tokens. It must be found in the /.well-known/openid-configuration/jwks.
Be aware that API's and clients cache the downloaded keys for 24 hours by default.
In production you need to use this method to add the signing key that you want to sign your tokens with
AddSigningCredential
See the documentation here
In production you should not use this method AddDeveloperSigningCredential.

Categories