ASP.NET Core 2.0 app.UseAuthentication не выполняется для каждого запроса - PullRequest
0 голосов
/ 01 июня 2018

У меня действительно странная проблема, которую я потратил целый день на отладку, и нигде не был близок к ее решению.Я нахожусь в процессе обновления моего приложения с 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?

1 Ответ

0 голосов
/ 06 июня 2018

Для тех, кто сталкивается с той же проблемой;моей проблемой оказалось единство.ASP.NET Core 2.0 не поддерживает Unity из коробки, потому что метод ConfigureServices() в Startup.cs является заменой стороннего DI-контейнера, такого как Unity или Autofac.Однако, если вы все еще хотите использовать Unity, вам нужно Nuget Unity.Microsoft.DependencyInjection для вашего проекта.В репозитории Github есть подробности о том, как его подключить.

Кроме того, все другие зависимые проекты использовали Unity 4.0.1, в которой IUnityContainer находится под Microsoft.Practices.Unity, тогда как в Unity 5 и выше IUnityContainer был перемещен.к Unity пространству имен.Это был дополнительный уловок, благодаря которому даже после настройки DI-контейнера в соответствии с инструкциями Github-репо я получал исключения с ошибочными разрешениями зависимостей.Обходным решением было создание нового UnityContainer с использованием Microsoft.Practices.Unity, позволить зависимым проектам загрузить его и затем скопировать эти регистрации в IUnityContainer в пространстве имен Unity.

Startup.cs

public void ConfigureContainer(IUnityContainer container)
{
     container.RegisterServices(Configuration);
}

UnityRegistrations.cs

public static void RegisterServices(this IUnityContainer container, IConfiguration configuration)
{
    // Microsoft.Practices.Unity
    var currentContainer = new UnityContainer();
    // Bootstrap this and register dependencies
    // Then copy them over
    foreach (var registration in currentContainer.Registrations)
    {
        container.RegisterType(registration.RegisteredType, registration.MappedToType);
    }
}
...