web-dev-qa-db-fra.com

Authentification de base dans ASP.NET Core

Question

Comment implémenter l'authentification de base avec adhésion personnalisée dans une application Web ASP.NET Core?

Notes

  • Dans MVC 5, j’utilisais les instructions de cet article qui nécessite l’ajout d’un module dans le WebConfig.

  • Je suis toujours en train de déployer mon nouveau MVC Coreapplication sur IIS mais cette approche ne semble pas fonctionner.

  • De plus, je ne souhaite pas utiliser le support intégré de IIS pour l'authentification de base, car il utilise les informations d'identification Windows.

40
A-Sharabiani

La sécurité ASP.NET n'inclut pas le middleware d'authentification de base en raison de ses problèmes potentiels d'insécurité et de performances.

Si vous avez besoin d'un middleware d'authentification de base à des fins de test, veuillez consulter https://github.com/blowdart/idunno.Authentication

24
blowdart

ASP.NET Core 2.0 a introduit des modifications radicales à Authentication and Identity.

Sur 1.x, les fournisseurs d'authentification étaient configurés via un middleware (en tant qu'implémentation de la réponse acceptée). Le 2.0 est basé sur les services.

Détails sur la documentation MS: https://docs.Microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x

J'ai écrit une implémentation d'authentification de base pour ASP.NET Core 2.0 et ai publié sur NuGet: https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic

12
Bruno Garcia

Nous avons implémenté la sécurité Digest pour un service interne en utilisant un 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);
    }
}

Ensuite, vous pouvez annoter les contrôleurs ou les méthodes auxquelles vous souhaitez accéder avec la sécurité Digest:

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

}

Pour implémenter la sécurité de base, il suffit de changer DigestAuthenticationFilterAttribute pour ne pas utiliser SHA1 mais le décodage direct Base64 de l'en-tête Authorization.

Je suis déçu par la conception du middleware d'authentification ASP.NET Core. En tant que cadre, il devrait simplifier et conduire à une productivité accrue, ce qui n’est pas le cas ici.

Quoi qu’il en soit, une approche simple mais sûre repose sur les filtres d’autorisation, par exemple. IAsyncAuthorizationFilter. Notez qu'un filtre d'autorisation sera exécuté après les autres middlewares, lorsque MVC choisit une action du contrôleur et passe au traitement du filtre. Mais dans les filtres, les filtres d'autorisation sont d'abord exécutés ( détails ).

J'allais juste commenter les commentaires de Clays sur la réponse de Hector, mais je n'aimais pas que l'exemple de Hectors lève des exceptions et ne dispose d'aucun mécanisme de contestation; voici donc un exemple concret.

Garder en tete:

  1. L'authentification de base sans HTTPS en production est extrêmement mauvaise. Assurez-vous que vos paramètres HTTPS sont renforcés (par exemple, désactivez tous les protocoles SSL et TLS <1.2, etc.).
  2. Aujourd'hui, l'authentification de base est principalement utilisée pour exposer une API protégée par une clé d'API (voir Stripe.NET, Mailchimp, etc.). Crée des API conviviales pour le curl qui sont aussi sécurisées que les paramètres HTTPS sur le serveur.

En gardant cela à l’esprit, n’acceptez aucun des principes de l’authentification FUD. Sauter quelque chose d'aussi élémentaire que l'authentification de base est une question d'opinion peu convaincante. Vous pouvez voir la frustration autour de cette conception dans les commentaires ici .

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

Et ce sont les cours de soutien

    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