The goal is to use a Blazor server as a relay server using signalR.
I have little to no experience with blazor servers before this.
The Idea would be to connect a Winform/Xamarin client to this server, target the recipient using a name/id from an existing database, and relay the necessary info.
Hub:
[Authorize]
public class ChatHub : Hub
{
public Task SendMessageAsync(string user, string message)
{
//Context.UserIdentifier
Debug.WriteLine(Context.UserIdentifier);
Debug.WriteLine(Context?.User?.Claims.FirstOrDefault());
return Clients.All.SendAsync("ReceiveMessage", user, message); ;
}
public Task DirectMessage(string user, string message)
{
return Clients.User(user).SendAsync("ReceiveMessage", user, message);
}
}
As per documentation I'm trying to set the Context.UserIdentifier, I do however struggle with the authentication part. My program.cs looks like this:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddTransient<IUserIdProvider, MyUserIdProvider>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
//var accessToken = context.Request.Query["access_token"];
var accessToken = context.Request.Headers["Authorization"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chathub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSignalR();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/chathub");
app.MapFallbackToPage("/_Host");
app.Run();
As for my Client (a winform test client) I tried something like this:
HubConnection chatHubConnection;
chatHubConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:7109/chathub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
private async void HubConBtn_Click(object sender, EventArgs e)
{
chatHubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await chatHubConnection.StartAsync();
MessagesLB.Items.Add("Connected!");
HubConBtn.Enabled = false;
SendMessageBtn.Enabled = true;
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
As a first step I'm just trying to authenticate a user/check that it's in the live database, if so connect and fill out: Context.UserIdentifier so I can use this within the Hub. I understand that I probably need a middleware however I don't really know exactly how to test a connectionId/Jwt token or similar to get the user/connection.
Any nudge in the right direction would be appreciated.
If I understand your question you don't know where and how to generate a JWT token.
For me the JWT token should be generated from the server, your hub.
POST api/auth and in the playload you give login + SHA256 password and returns JWT token.
Once you checked the user auth is correct in you DB you can issue the token.
To generate a JWT token I use this piece of code.
public string GenerateToken(IConfiguration Config, DateTime? expire)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, _id),
new Claim(ClaimsIdentity.DefaultRoleClaimType, role)
};
// ClaimsIdentity.DefaultRoleClaimType
var bytes = Encoding.UTF8.GetBytes(Config["jwt:Secret"]);
var key = new SymmetricSecurityKey(bytes);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
var token = new JwtSecurityToken(
//Config.GetValue<string>("jwt:Issuer"),
//Config.GetValue<string>("jwt:Issuer") + "/ressources",
claims: claims,
expires: DateTime.Now.AddMinutes(Config.GetValue<int>("jwt:ExpireMinute")),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
#edit
Look here to allow JWT for SignalR
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0
I also added this.
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
The easiest solution would be to use something like IdentityServer to handle the authentication. It's a free solution, also .NET based which takes very little configuration effort to offer you simple client credentials authentication and generate the token for you.
I did basically exactly what you're asking here: A WinForms application connecting to my signalR hub application on a remote server, using Bearer token - but I also have OIDC/OAUTH implemented with third party user account login.
IdentityServer offers a great repository of full examples that showing you all the flow - and with just a few lines of code changed, you have a fullblown authentication system, which can be enhanced easily.
With IdentityServer you get everything, even the corresponding extension methods that enable your signalR hub application to create the claims principal (aka user) from the claims included within your token.
Here you'll find all the examples and docs:
https://github.com/IdentityServer/IdentityServer4
If you hit any walls, just reply here and I'll try to help.
Related
I am new to identity server and know very little about cookie management.
I have configured a web api and a client app with IDS4. below is my startup.cs in identity server.
public void ConfigureServices(IServiceCollection services)
{
//...
var builder = services.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
options.IssuerUri = "https://localhost:5001";
})
//...
}
and here is my extension method to add authentication in web api
public static void AddCustomAuthentication(this IServiceCollection services, IConfiguration Configuration)
{
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
// tell the system base address of identity server
options.Authority = Configuration["IdentityServerUri"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
and here is my extension method to add authentication in client app
public static void AddCustomAuthentication(this IServiceCollection services, IConfiguration Configuration)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(options =>
{
options.Cookie.Name = "CorpLense.HR.WebClient";
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration["IdentityServerUri"];
options.SignInScheme = "Cookies";
options.ClientId = "CorpLense.HR.WebClient";
options.ClientSecret = "***";
options.ResponseType = "code";
options.SaveTokens = true;
// get claims
// request access to scopes
});
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
}
and below is a screenshot of the cookies
OBSERVATIONS
I have noticed that my client app stays logged-in all the time. it does not ask me to login even the next day. however, after a every few hours my web API throws UNAUTHORIZED response codes. And when I logout and login again from my client app, the api starts working fine again. some thing tells me that perhaps the bearer token gets expired.
OBJECTIVE
I just want to know how to have total control on cookies. I want the client app to automatically logout when the cookie on the server side has expired, or the cookie on web api side has expired, or the cookie on the client app has expired.
I also want to know how to control cookie expirations times. and how to refresh bearer tokens if they expire.
If I am not wrong, the default lifetime for the access token is 1 hour and you need to implement support for refresh token to renew the token when the access token is about to expire.
You can also configure the client cookie lifetime in AddCookie(...).
Does this help you?
see https://docs.duendesoftware.com/identityserver/v5/bff/extensibility/tokens/
I'm using .NET Core 3.1 with Identity Server 4 and connecting to Azure AD via OpenIdConnect. I'm using a Vue.js front-end and .NET Core API. IdentityServer, the front-end, and the API are all hosted on-prem on the same server (same domain). Everything uses https. I'm using an Oracle database with EF model first, with fully-customized IdentityServer stores and a custom user store (I implemented the interfaces). I'm using IdentityServer's Quickstart, edited a little to hook up my custom user store instead of the test user. I'm running this in my dev environment.
If I type in the url to the IdentityServer, I'm redirected to Azure AD, signed-in successfully, and shown this page:
Grants - successful login
The claims are coming back from Azure AD and the auto-provisioning is successful. It is written successfully to the database.
Authenticating through my JS client hits IdentityServer, redirects to Azure AD, I sign-in, then it redirects to IdentityServer's ExternalController, then redirects back to a Microsoft url, then proceeds to repeat until it finally fails with this page:
Sign-in failure from Azure AD
My guess is I messed up a redirect uri somewhere. Here is my code and the IdentityServer log:
IdentityServer Log
That block of logging repeats 6-10 times. No errors or anything different at the end.
I had to break up the C# code because the site couldn't handle one of my long options lines.
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Client OIDC config:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
Callback method being hit for ExternalController:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD config:
redirect uri: [IdentityServer url]/callback-aad
Database table data:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
Please let me know if you need any additional information. Thank you
The problem was in my custom UserStore. I was getting the user by the Azure AD SubjectId instead of the UserSubjectId. So in the ExternalController, the ApplicationUser object was coming up as null. Instead of an exception, it kept going back to Azure AD to try to get the user again, but obviously that just creates an infinite loop. I didn't think to look there since my user was successfully provisioned with Id's and claims.
I've got an app which is hosting simultaneously Identity Server 4 and a client app (Vue) which uses a couple of rest services defined in an area for managing the site. The idea is that users associated with a specific role can access the client app and call the rest services for performing the actions.
Currently, my problem is that when the api return 302 when the user doesn't belong to the admin role. I'd like to change this to a 401, but I'm having some problems with it.
If this was a simple aspnet core app, then I'd simply pass a lambda to the OnRedirectToLogin property of the cookie handler that takes care of the request. Unfortunately, IS4 will only allow me to set a couple of basic settings of the cookie (expiration and sliding). The same docs say that I can override the cookie handler. So, I've tried doing the following:
services.AddIdentityServer()
... // other configurations
services.AddAuthentication(sharedOptions => {
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//IdentityServerConstants.ExternalCookieAuthenticationScheme;
sharedOptions.DefaultChallengeScheme = IdentityServerConstants.SignoutScheme;
})
... //other external providers...
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options => {
options.Events = new CookieAuthenticationEvents {
OnRedirectToLogin = ctx => {
if (ctx.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase)) {
ctx.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
};
});
I expected to seem my handler being called whenever a request is redirected to the login page, but it never happens. Can anyone help?
Thanks
EDIT: just to add that I'm also using aspnet identity for managing the user accounts...
Posting the answer here in case anyone is interested...
After some digging, I've found that using identity means that you can't customize the cookie handler by doing what I was doing. Fortunately, the ConfigureAuthenticationEvent that can be configured by the ConfigureApplicationCookie extension method already does the right thing: if it detects that the current request is an AJAX call, it will return 401; if not, it will return 302. And here was the problem: the request made from the vue client wasn't being considered an AJAX request because it wasn't setting the X-Request-With header to XMLHttpRequest.
So, all it was required was to configure axios to set the header in all the calls:
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
I wrote a middleware sometime ago for this exact purpose and never looked back so if you don't find better solution, perhaps the solution can help you as well:
public class RedirectHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RedirectHandlingMiddleware> _logger;
public RedirectHandlingMiddleware(RequestDelegate next, ILogger<RedirectHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
await HandleRedirect(context, ex);
await _next(context);
}
private Task HandleRedirect(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/Admin", StringComparison.OrdinalIgnoreCase) && context.Response.StatusCode == 302)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
return Task.CompletedTask;
}
}
Just need to register in Startup.cs:
app.UseAuthentication();
app.UseMiddleware<RedirectHandlingMiddleware>();
I know there are similar questions out there, but still it's not very clear,
after reading a bunch of posts related to the subject, this how i "understood" the code should look like, i am still dealing with all the concepts involved in oauth/openid/owin/katana/identityserver etc...
Big picture is: i have an angular application,
where the user register and log in, no consent is needed, once the user is logged in, the SPA will start comunicating with all the api's in the back and the api's should be able to authenticate against the auth server.
So basically,i need my web api to be able to authenticate in identity server 4, through client credentials grant type, with the issued token by the authentication server.
I got this client(web api 2 .net framework 4.5) defined in identiy server 4:
public static IEnumerable<Client> GetClients()
{
//client credentials client
return new List<Client>
{
new Client
{ ClientId = "client2",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api2" }
},
}
In the .net Api side i have this:
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType =
CookieAuthenticationDefaults.AuthenticationType
});
app.UseOpenIdConnectAuthentication(new
OpenIdConnectAuthenticationOptions
{
ClientId = "client2",
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ResponseType = "id_token",
Scope = "api2",
SignInAsAuthenticationType =
CookieAuthenticationDefaults.AuthenticationType,
}
});
And the controllers are decorated with the Autorize decorator.
These are the versions of the packages im using
id="Microsoft.Owin.Security.OpenIdConnect" version="4.0.0"
id="Microsoft.Owin.Security.OAuth" version="4.0.0"
id="Microsoft.Owin.Security" version="4.0.0"
id="Microsoft.Owin" version="4.0.0"
By the moment i am using one of the demo projects from the offical project site(https://github.com/IdentityServer/IdentityServer4.Samples), i added an extra call in the MVC demo app to call my api.
public async Task<IActionResult> CallApiUsingUserAccessToken2()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var content = await
client.GetStringAsync("http://localhost:17307/api
/Organization/GetOrganizationById/2007");
ViewBag.Json = JArray.Parse(content).ToString();
return View("Json");
}
According to the working demo, there are two ways to do this, but none have worked to me.
public async Task<IActionResult> CallApiUsingClientCredentials2()
{
var tokenClient = new TokenClient("http://localhost:5000/connect/token", "mvc", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var content = await client.GetStringAsync("http://localhost:17307/api/Organization/GetOrganizationById/2007");
ViewBag.Json = JArray.Parse(content).ToString();
return View("Json");
}
This is part of the response with the error, i am getting in both scenarios:
<div class="row">
<div class="col-sm-6">
<div class="alert alert-danger">
Sorry, there was an error
<strong>
<em>
: invalid_request
</em>
</strong>
<div>Invalid redirect_uri</div>
</div>
<div class="request-id">Request Id: 0HLIALF7L4N8J:00000001</div>
</div>
</div>
What is missing here or what is wrong, is the redirect_uri mandatory, why is not present in the configuration section for the .net core ?
This is how configuration of the api looks like in .net core and works fine.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
}
Thanks in advance.
Update
After some experimenting, i comfirming the issue i am having is in the api validating the access token using owin middleware.
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string,
string>
();
app.UseIdentityServerBearerTokenAuthentication
(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
RequiredScopes = new[] { "api2" },
});
}
I am using identityserver3.accesstokenvalidation to perform the validation, as it is recomended, but after getting the access token in the client application and pass it to the api request, i am getting a 401 unauthorized error, is this because it is expecting to operate under secure HTTPS ?, i notice for accesstokenvalidation v4 you can set "RequireHttpsMetadata = false" but i dont see this in v3, could be this the reason i am not getting the token validating ?
Try using the correct client_id first on this line from "mvc" to "client2"
var tokenClient = new TokenClient("http://localhost:5000/connect/token", "mvc", "secret");
I am trying to add a middleware to implement throttling in my Web API based on client id. This Web API is protected by Identity Server 4 and the JWT authentication handler.
The problem is that Context.User.Claims is always empty when my middleware runs.
I understand that the Jwt handler only gets called when the request hits the Authorize attribute.
Thus, my question is, how can I "force" the Jwt handler to run sooner in the pipeline so that my middleware gets the call after the token is validated and the client_id claim is available in the context principal?
Thanks for any help you can give me.
The code that setups the Web API is as follows:
public void ConfigureServices(IServiceCollection services)
{
// Validation
SmartGuard.NotNull(() => services, services);
// Log
this.Logger.LogTrace("Application services configuration starting.");
// Configuration
services
.AddOptions()
.Configure<ServiceConfiguration>(this.Configuration.GetSection(nameof(ServiceConfiguration)))
.Configure<TelemetryConfiguration>(this.Configuration.GetSection(nameof(TelemetryConfiguration)))
.Configure<TableStorageServiceConfiguration>(this.Configuration.GetSection(nameof(TableStorageServiceConfiguration)))
.UseConfigurationSecrets();
ServiceConfiguration serviceConfiguration = services.ResolveConfiguration<ServiceConfiguration>();
// Telemetry (Application Insights)
services.AddTelemetryForApplicationInsights();
// Memory cache
services.AddDistributedMemoryCache();
// MVC
services.AddMvc();
// Identity
services
.AddAuthorization(
(options) =>
{
options.AddPolicy(
Constants.Policies.Settings,
(policy) =>
{
policy.RequireClaim(Constants.ClaimTypes.Scope, Scopes.Settings);
});
});
// NOTE:
// We are using the JWT Bearer handler here instead of the IdentityServer handler
// because version 2.3.0 does not handle bearer challenges correctly.
// For more info: https://github.com/IdentityServer/IdentityServer4/issues/2047
// This is supposed to be fixed in version 2.4.0.
services
.AddAuthentication(Constants.AuthenticationSchemes.Bearer)
.AddJwtBearer(
(options) =>
{
options.Authority = serviceConfiguration.IdentityServerBaseUri;
options.Audience = Constants.ApiName;
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true;
options.RefreshOnIssuerKeyNotFound = true;
options.SaveToken = true;
options.Events = new JwtBearerEvents()
{
OnChallenge = HandleChallenge
};
});
// Web API Versioning
services.AddApiVersioning(
(options) =>
{
options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(ApiVersions.DefaultVersion.Major, ApiVersions.DefaultVersion.Minor);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
});
// Setup Throttling
services
.AddThrottling()
.AddClientRateHandler(this.Configuration.GetSection(nameof(ClientRateThrottlingOptions)));
// Routes analyzer
// Creates the /routes route that lists all the routes configured
services.AddRouteAnalyzerInDevelopment(this.CurrentEnvironment);
// Add the managers
services.AddManagers();
// Background services
services.AddBackgroundService<StorageSetupService>();
// Log
this.Logger.LogTrace("Application services configuration completed.");
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Validation
SmartGuard.NotNull(() => app, app);
SmartGuard.NotNull(() => env, env);
// Log
this.Logger.LogTrace("Application configuration starting.");
// Error handling (Telemetry)
app.UseTelemetryExceptionHandler();
// Authentication
app.UseAuthentication();
// Register the throttling middleware
app.UseThrottling();
// MVC
app.UseMvc(
(routes) =>
{
// Routes analyzer
routes.MapRouteAnalyzerInDevelopment(env);
});
// Log
this.Logger.LogTrace("Application configuration completed.");
}
The relevant middleware code is as follows:
internal class ClientRateMiddleware : IClientRateThrottlingMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
(...)
Claim claim = context.User.FindFirst("client_id");
// Claim is always null here because the Jwt handler has not run
(...)
}
}
OK, so I think I have kind of cracked this one. I think #Hugo Quintela Ribeiro is right about the authorization only occurring when the [Authorize] filter is hit, or when a controller that does not [Allow Anonymous] is hit in the case that authorization is set for the whole app. This of course happens at the controllers, and not in the middleware.
It turns out you can actually force authentication to occur in the middleware. I tried a couple of things like the following with no success.
await context.AuthenticateAsync();
await context.AuthenticateAsync("Custom"); //name of my jwt auth
In the end, I had to inject IAuthorizationPolicyProvider and IPolicyEvaluator to get the default policy and authenticate it.
using cpDataORM;
using cpDataServices.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace cpDataASP.Middleware
{
public class LocalizationAndCurrencyMiddleware
{
private readonly RequestDelegate _next;
public LocalizationAndCurrencyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, IUserService _userService, ILoginContextAccessor loginContext, IAuthorizationPolicyProvider policyProvider, IPolicyEvaluator policyEvaluator)
{
var policy = await policyProvider.GetDefaultPolicyAsync();
await policyEvaluator.AuthenticateAsync(policy, context);
var localizationResources = await _userService.GetLocalizationResources();
loginContext.Timezone = localizationResources.Timezone;
CultureInfo.CurrentCulture = localizationResources.Culture;
await _next.Invoke(context);
}
}
}