wake-up-neo.com

Grundlegende Authentifizierung in ASP.NET Core

Frage

Wie kann ich die Standardauthentifizierung mit benutzerdefinierter Mitgliedschaft in einer ASP.NET Core-Webanwendung implementieren?

Notizen

  • In MVC 5 habe ich die Anweisungen in diesem Artikel verwendet, wofür ein Modul in WebConfig hinzugefügt werden muss.

  • Ich setze immer noch mein neues MVC Coreapplication on IIS aber dieser Ansatz scheint nicht zu funktionieren.

  • Ich möchte auch nicht die integrierte IIS Unterstützung für die Standardauthentifizierung verwenden, da die Windows-Anmeldeinformationen verwendet werden.

40
A-Sharabiani

In ASP.NET Security wird die Middleware für die Basisauthentifizierung aufgrund potenzieller Unsicherheits- und Leistungsprobleme nicht enthalten sein.

Wenn Sie zu Testzwecken Middleware für die Basisauthentifizierung benötigen, lesen Sie bitte https://github.com/blowdart/idunno.Authentication

24
blowdart

In ASP.NET Core 2.0 wurden grundlegende Änderungen an Authentifizierung und Identität vorgenommen.

Auf 1.x wurden Authentifizierungsanbieter über Middleware konfiguriert (als Implementierung der akzeptierten Antwort). Auf 2.0 basiert es auf Diensten.

Details zu MS doc: https://docs.Microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

Ich habe eine Basic Authentication-Implementierung für ASP.NET Core 2.0 geschrieben und in NuGet veröffentlicht: https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic

12
Bruno Garcia

Wir haben Digest-Sicherheit für einen internen Service mithilfe eines ActionFilter implementiert:

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);
    }
}

Anschließend können Sie die Controller oder Methoden, auf die mit Digest-Sicherheit zugegriffen werden soll, mit Anmerkungen versehen:

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

}

Um die Basissicherheit zu implementieren, ändern Sie einfach das DigestAuthenticationFilterAttribute so, dass SHA1 nicht verwendet wird, sondern die Base64-Dekodierung des Authorization-Headers erfolgt.

Ich bin vom Design der ASP.NET Core-Authentifizierungs-Middleware enttäuscht. Als Rahmen sollte es vereinfachen und zu einer höheren Produktivität führen, was hier nicht der Fall ist.

Auf jeden Fall basiert ein einfacher und dennoch sicherer Ansatz auf den Autorisierungsfiltern, z. IAsyncAuthorizationFilter. Beachten Sie, dass ein Berechtigungsfilter nach den anderen Middlewares ausgeführt wird, wenn MVC eine bestimmte Controller-Aktion auswählt und zur Filterverarbeitung übergeht. Innerhalb von Filtern werden jedoch zuerst Berechtigungsfilter ausgeführt ( Details ).

Ich wollte gerade Clays Kommentar zu Hectors Antwort kommentieren, aber ich mochte es nicht, wenn Hectors Beispiele Ausnahmen warf und keinen Herausforderungsmechanismus hatte, also hier ein funktionierendes Beispiel.

Merken Sie sich:

  1. Die Standardauthentifizierung ohne HTTPS in der Produktion ist extrem schlecht. Stellen Sie sicher, dass Ihre HTTPS-Einstellungen gesichert sind (z. B. deaktivieren Sie alle SSL- und TLS-Einstellungen <1.2 usw.).
  2. Heutzutage wird die Standardauthentifizierung hauptsächlich verwendet, wenn eine API verfügbar gemacht wird, die durch einen API-Schlüssel geschützt ist (siehe Stripe.NET, Mailchimp usw.). Ermöglicht lockenfreundliche APIs, die genauso sicher sind wie die HTTPS-Einstellungen auf dem Server.

In diesem Sinne sollten Sie sich nicht in die FUD zur Basisauthentifizierung einlassen. Das Überspringen von etwas so Grundlegendem wie der grundlegenden Authentifizierung hat eine hohe Meinung und eine niedrige Substanz. Sie können die Frustration um diesen Entwurf in den Kommentaren sehen hier .

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));
        }
    }
}

Und das sind die unterstützenden Klassen

    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);
        }
    }
0
DeepSpace101