У меня действительно странная проблема, которую я потратил целый день на отладку, и нигде не был близок к ее решению.Я нахожусь в процессе обновления моего приложения с ASP.NET Core 1.x до 2.1.Как часть этого, мне приходится заново подключать механизм аутентификации и авторизации.Мы используем аутентификацию JWTBearer, а я использую почтальон для запуска вызова API, который выполняет конвейер, и я вижу, как выполняется AuthHandler.Однако, если я выполню тот же запрос еще раз, AuthHandler не выполнит и отладчик "перешагнет" вызов context.AuthenticateAsync и вернет предыдущий результат.Для уточнения я написал собственный обработчик аутентификации, который является копией JWTAuthHandler.Код для создания пользовательского обработчика основан на ответе здесь.
using Microsoft.AspNetCore.Authentication.JwtBearer;
public class CustomAuthOptions : JwtBearerOptions
{
}
using Microsoft.AspNetCore.Authentication;
public static class CustomAuthExtensions
{
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
{
return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>("CustomScheme", configureOptions);
}
}
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
private OpenIdConnectConfiguration _configuration;
public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new JwtBearerEvents Events
{
get => (JwtBearerEvents)base.Events;
set => base.Events = value;
}
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new JwtBearerEvents());
/// <summary>
/// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
// event can set the token
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers["Authorization"];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("CustomAuth ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("CustomAuth ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
var issuers = new[] { _configuration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
?? _configuration.SigningKeys;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal;
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
////Logger.TokenValidationFailed(ex);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
////Logger.TokenValidationSucceeded();
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
////Logger.ErrorProcessingMessage(ex);
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var authResult = await HandleAuthenticateOnceSafeAsync();
var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties)
{
AuthenticateFailure = authResult?.Failure
};
// Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token).
if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null)
{
eventContext.Error = "invalid_token";
eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
}
await Events.Challenge(eventContext);
if (eventContext.Handled)
{
return;
}
Response.StatusCode = 401;
if (string.IsNullOrEmpty(eventContext.Error) &&
string.IsNullOrEmpty(eventContext.ErrorDescription) &&
string.IsNullOrEmpty(eventContext.ErrorUri))
{
Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
}
else
{
// https://tools.ietf.org/html/rfc6750#section-3.1
// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
var builder = new StringBuilder(Options.Challenge);
if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0)
{
// Only add a comma after the first param, if any
builder.Append(',');
builder.Append(',');
}
if (!string.IsNullOrEmpty(eventContext.Error))
{
builder.Append(" error=\"");
builder.Append(eventContext.Error);
builder.Append("\"");
}
if (!string.IsNullOrEmpty(eventContext.ErrorDescription))
{
if (!string.IsNullOrEmpty(eventContext.Error))
{
builder.Append(",");
}
builder.Append(" error_description=\"");
builder.Append(eventContext.ErrorDescription);
builder.Append('\"');
}
if (!string.IsNullOrEmpty(eventContext.ErrorUri))
{
if (!string.IsNullOrEmpty(eventContext.Error) ||
!string.IsNullOrEmpty(eventContext.ErrorDescription))
{
builder.Append(",");
}
builder.Append(" error_uri=\"");
builder.Append(eventContext.ErrorUri);
builder.Append('\"');
}
Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
}
}
private static string CreateErrorDescription(Exception authFailure)
{
IEnumerable<Exception> exceptions;
if (authFailure is AggregateException agEx)
{
exceptions = agEx.InnerExceptions;
}
else
{
exceptions = new[] { authFailure };
}
var messages = new List<string>();
foreach (var ex in exceptions)
{
// Order sensitive, some of these exceptions derive from others
// and we want to display the most specific message possible.
switch (ex)
{
case SecurityTokenInvalidAudienceException _:
messages.Add("The audience is invalid");
break;
case SecurityTokenInvalidIssuerException _:
messages.Add("The issuer is invalid");
break;
case SecurityTokenNoExpirationException _:
messages.Add("The token has no expiration");
break;
case SecurityTokenInvalidLifetimeException _:
messages.Add("The token lifetime is invalid");
break;
case SecurityTokenNotYetValidException _:
messages.Add("The token is not valid yet");
break;
case SecurityTokenExpiredException _:
messages.Add("The token is expired");
break;
case SecurityTokenSignatureKeyNotFoundException _:
messages.Add("The signature key was not found");
break;
case SecurityTokenInvalidSignatureException _:
messages.Add("The signature is invalid");
break;
}
}
return string.Join("; ", messages);
}
}
И затем Startup.cs для его подключения:
public class Startup
{
/// <summary>
/// Initializes a new instance of the <see cref="Startup"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
ConfigureLogging();
}
/// <summary>
/// Gets the configuration.
/// </summary>
/// <value>
/// The configuration.
/// </value>
public IConfiguration Configuration { get; }
/// <summary>
/// Gets or sets the Container
/// </summary>
private IUnityContainer Container { get; set; }
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var logger = Logger.For(this).ForAction(nameof(ConfigureServices));
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);
services.AddResponseCompression();
logger.Info("Configuring JWT Bearer Token Authorization...");
services.AddAuthentication(options =>
{
// the scheme name has to match the value we're going to use in AuthenticationBuilder.AddScheme(...)
options.DefaultAuthenticateScheme = "CustomScheme";
options.DefaultChallengeScheme = "CustomScheme";
})
.AddCustomAuth(options => {
options.Audience = this.Configuration.ObtainConfiguredString(ConfigurationKeys.ValidAudienceId);
options.Authority = this.Configuration.ObtainConfiguredString(ConfigurationKeys.IssuerId);
options.SaveToken = false;
options.TokenValidationParameters = new TokenValidationParameters().WithConfiguredParameters(this.Configuration);
});
logger.Info("Adding Authorization policies to Services...");
services.AddAuthorization(
options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder("CustomScheme").RequireAuthenticatedUser().Build();
});
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IAuthenticationHandler, CustomAuthHandler>();
EnableCors(services);
logger.Info("Adding MVC support to Services...");
services.AddMvc(config =>
{
var defaultPolicy = new AuthorizationPolicyBuilder(new[] { "CustomScheme" })
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
Container = new UnityContainer();
logger.Info("Registering other Services with UnityContainer...");
Container.RegisterServices(Configuration);
// Configure Microsoft DI for Unity resolution
logger.Info("Configuring ASP.Net Core service resolution to use UnityContainer...");
return services.UseUnityResolution(Container, s => s.BuildServiceProvider());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// <summary>
/// The Configure
/// </summary>
/// <param name="app">The app<see cref="IApplicationBuilder"/></param>
/// <param name="env">The env<see cref="IHostingEnvironment"/></param>
/// <param name="loggerFactory">The loggerFactory<see cref="ILoggerFactory"/></param>
/// <param name="memoryCache">The memoryCache<see cref="IMemoryCache"/></param>
/// <param name="contextAccessor">The contextAccessor<see cref="IHttpContextAccessor"/></param>
/// <param name="authzClient">The authzClient<see cref="IAuthzClient"/></param>
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IMemoryCache memoryCache,
IHttpContextAccessor contextAccessor,
IAuthzClient authzClient)
{
var logger = Logger.For(this).ForAction(nameof(Configure));
logger.Info("Configuring ASP.Net Core logging framework...");
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
var corsEnabled = this.Configuration.ObtainConfiguredBooleanWithDefault(ConfigurationKeys.EnableCors, false);
if (corsEnabled)
{
app.UseCors("CorsPolicy");
}
logger.Info("Configuring ASP.Net Core custom status page...");
app.UseStatusCodePagesWithReExecute("/error/{0}");
if (env.IsDevelopment())
{
logger.Info("Configuring development middle-ware...");
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
logger.Info("Configuring standard ASP.Net Core behaviors...");
app.UseDefaultFiles();
app.UseStaticFiles();
////app.UseAuthentication();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
var result = await context.AuthenticateAsync("CustomScheme");
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await next.Invoke();
});
app.UseMvc();
app.WithRequestLogging();
}
private void EnableCors(IServiceCollection service)
{
var logger = Logger.For(this).ForAction(nameof(EnableCors));
var corsEnabled = this.Configuration.ObtainConfiguredBooleanWithDefault(ConfigurationKeys.EnableCors, false);
if (corsEnabled)
{
logger.Verbose("Configuring ASP.Net Core CORS support...");
service.AddCors(
options =>
{
options.AddPolicy("CorsPolicy",
builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyHeader();
builder.AllowAnyMethod();
builder.AllowCredentials();
});
});
}
}
}
}
Можеткто-нибудь подскажите пожалуйста что я делаю не так?В первый раз, когда я запускаю запрос почтальона с правильным AuthorizationHeader с токеном доступа, эта строка выполняет CustomAuthHandler:
var result = await context.AuthenticateAsync("CustomScheme");
Однако во второй раз отладчик перешагивает этот код?Это доводит меня до стены.Мне, должно быть, не хватает чего-то простого!
РЕДАКТИРОВАТЬ: В версии Core 1.x ConfigureServices был настроен как
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var logger = Logger.For(this).ForAction(nameof(ConfigureServices));
logger.Verbose("Adding MVC support to Services...");
// Add framework services.
services.AddMvc();
logger.Verbose("Adding Authorization policies to Services...");
services.AddAuthorization(
options =>
{
options.AddPolicy(
"SomePermission",
policy => policy.RequireClaim("claimUrl", "Some Permission"));
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
container = new UnityContainer();
logger.Verbose("Registering other Services with UnityContainer...");
container.RegisterServices(Configuration);
// Configure Microsoft DI for Unity resolution
logger.Verbose("Configuring ASP.Net Core service resolution to use UnityContainer...");
return services.UseUnityResolution(container, s => s.BuildServiceProvider());
}
И Configure () был подключен какследует
app.UseAuth0JwtBearerAuthentication(
new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters =
new TokenValidationParameters().WithConfiguredParameters(this.Configuration)
});
if (env.IsDevelopment())
{
logger.Verbose("Configuring development middleware...");
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
logger.Verbose("Configuring standard ASP.Net Core behaviors...");
app.UseDefaultFiles();
app.UseMvc();
app.UseStaticFiles();
Используя эту версию, если я выполняю вызовы почтальона, я получаю новый ClaimsPrincipal для каждого запроса.Так что же изменилось в ASP.NET Core 2.1?