Базовая аутентификация в ядре ASP.NET

Вопрос

Как я могу реализовать базовую аутентификацию с пользовательским членством в веб-приложении ASP.NET Core?

Примечания

  • В MVC 5 я использовал инструкции в этой статье которая требует добавления модуля в WebConfig.

  • Я все еще развертываю свое новое приложение MVC Core на IIS, но этот подход, похоже, не работает.

  • Я также не хочу использовать встроенную поддержку IIS для базовой проверки подлинности, поскольку она использует учетные данные Windows.

Ответы

Ответ 1

Безопасность ASP.NET не будет содержать промежуточное ПО базовой аутентификации из-за его потенциальных проблем с безопасностью и производительностью.

Если вам требуется базовое средство проверки подлинности для тестирования, пожалуйста, посмотрите https://github.com/blowdart/idunno.Authentication

Ответ 2

В ASP.NET Core 2.0 были внесены серьезные изменения в аутентификацию и идентификацию.

На 1.x провайдеры аутентификации были настроены через Middleware (как принятая реализация ответа). На 2.0 это на основе сервисов.

Подробности на MS Doc: https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

Я написал реализацию базовой аутентификации для ASP.NET Core 2.0 и опубликовал в NuGet:https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic

Ответ 3

Мы реализовали защиту дайджеста для внутренней службы с помощью ActionFilter:

public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
    private const string AUTH_HEADER_NAME = "Authorization";
    private const string AUTH_METHOD_NAME = "Digest ";
    private AuthenticationSettings _settings;

    public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
    {
        _settings = settings.Value;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        ValidateSecureChannel(context?.HttpContext?.Request);
        ValidateAuthenticationHeaders(context?.HttpContext?.Request);
        base.OnActionExecuting(context);
    }

    private void ValidateSecureChannel(HttpRequest request)
    {
        if (_settings.RequireSSL && !request.IsHttps)
        {
            throw new AuthenticationException("This service must be called using HTTPS");
        }
    }

    private void ValidateAuthenticationHeaders(HttpRequest request)
    {
        string authHeader = GetRequestAuthorizationHeaderValue(request);
        string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
        if (string.IsNullOrEmpty(digest))
        {
            throw new AuthenticationException("You must send your credentials using Authorization header");
        }
        if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
        {
            throw new AuthenticationException("Invalid credentials");
        }

    }

    private string GetRequestAuthorizationHeaderValue(HttpRequest request)
    {
        return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
    }

    public static string CalculateSHA1(string text)
    {
        var sha1 = System.Security.Cryptography.SHA1.Create();
        var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
        return Convert.ToBase64String(hash);
    }
}

Впоследствии вы можете аннотировать контроллеры или методы, к которым вы хотите получить доступ, с помощью службы дайджеста:

[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
    [HttpGet]
    public string Get()
    {
        return "HELLO";
    }

}

Чтобы реализовать базовую безопасность, просто измените атрибут DigestAuthenticationFilterAttribute, чтобы не использовать SHA1, но напрямую декодировать Base64 заголовка авторизации.

Ответ 4

Я разочарован дизайном промежуточного программного обеспечения для аутентификации ASP.NET Core. В качестве основы это должно упростить и привести к большей производительности, что здесь не так.

В любом случае, простой, но безопасный подход основан на фильтрах авторизации, например IAsyncAuthorizationFilter. Обратите внимание, что фильтр авторизации будет выполняться после других промежуточных программ, когда MVC выбирает определенное действие контроллера и переходит к обработке фильтра. Но в фильтрах фильтры авторизации выполняются первыми (подробнее).

Я просто собирался прокомментировать комментарий Клэйса к ответу Гектора, но мне не понравился пример Гектора, выдающий исключения и не имеющий никакого механизма вызова, поэтому вот рабочий пример.

Имейте в виду:

  1. Обычная аутентификация без HTTPS в производстве крайне плохая. Убедитесь, что ваши настройки HTTPS усилены (например, отключите все SSL и TLS & lt; 1.2 и т.д.)
  2. В настоящее время основная часть обычной аутентификации используется для предоставления API, защищенного ключом API (см. Stripe.NET, Mailchimp и т.д.). Делает для скручивания API, которые так же безопасны, как настройки HTTPS на сервере.

Имея это в виду, не покупайте FUD вокруг базовой аутентификации. Пропуск чего-то столь же простого, как базовая аутентификация, имеет большое мнение и низкое содержание. Вы можете увидеть разочарование вокруг этого дизайна в комментариях здесь.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace BasicAuthFilterDemo
{
    public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
    {
        public string Realm { get; set; }
        public const string AuthTypeName = "Basic ";
        private const string _authHeaderName = "Authorization";

        public BasicAuthenticationFilterAttribute(string realm = null)
        {
            Realm = realm;
        }

        public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
        {
            try
            {
                var request = context?.HttpContext?.Request;
                var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
                string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
                if (string.IsNullOrEmpty(encodedAuth))
                {
                    context.Result = new BasicAuthChallengeResult(Realm);
                    return;
                }

                var (username, password) = DecodeUserIdAndPassword(encodedAuth);

                // Authenticate credentials against database
                var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
                var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
                var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();                
                if (!await userManager.CheckPasswordAsync(founduser, password))
                {
                    // writing to the Result property aborts rest of the pipeline
                    // see https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
                    context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
                }

                // Populate user: adjust claims as needed
                var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
                var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
                context.HttpContext.User = principal;
            }
            catch
            {
                // log and reject
                context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
            }
        }

        private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
        {
            var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
            var separator = userpass.IndexOf(':');
            if (separator == -1)
                return (null, null);

            return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
        }
    }
}

А это вспомогательные классы

    public class StatusCodeOnlyResult : ActionResult
    {
        protected int StatusCode;

        public StatusCodeOnlyResult(int statusCode)
        {
            StatusCode = statusCode;
        }

        public override Task ExecuteResultAsync(ActionContext context)
        {
            context.HttpContext.Response.StatusCode = StatusCode;
            return base.ExecuteResultAsync(context);
        }
    }

    public class BasicAuthChallengeResult : StatusCodeOnlyResult
    {
        private string _realm;

        public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
        {
            _realm = realm;
        }

        public override Task ExecuteResultAsync(ActionContext context)
        {
            context.HttpContext.Response.StatusCode = StatusCode;
            context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
            return base.ExecuteResultAsync(context);
        }
    }