Autenticación de API web ASP.NET Core

98

Estoy luchando con la forma de configurar la autenticación en mi servicio web. El servicio se compila con la API web ASP.NET Core.

Todos mis clientes (aplicaciones WPF) deben usar las mismas credenciales para llamar a las operaciones del servicio web.

Después de investigar un poco, se me ocurrió la autenticación básica: enviar un nombre de usuario y una contraseña en el encabezado de la solicitud HTTP. Pero después de horas de investigación, me parece que la autenticación básica no es el camino a seguir en ASP.NET Core.

La mayoría de los recursos que encontré están implementando la autenticación mediante OAuth o algún otro middleware. Pero eso parece ser demasiado grande para mi escenario, además de usar la parte de Identidad de ASP.NET Core.

Entonces, ¿cuál es la forma correcta de lograr mi objetivo: autenticación simple con nombre de usuario y contraseña en un servicio web ASP.NET Core?

¡Gracias por adelantado!

Felix
fuente

Respuestas:

75

Puede implementar un middleware que maneje la autenticación básica.

public async Task Invoke(HttpContext context)
{
    var authHeader = context.Request.Headers.Get("Authorization");
    if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
    {
        var token = authHeader.Substring("Basic ".Length).Trim();
        System.Console.WriteLine(token);
        var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
        var credentials = credentialstring.Split(':');
        if(credentials[0] == "admin" && credentials[1] == "admin")
        {
            var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
            var identity = new ClaimsIdentity(claims, "Basic");
            context.User = new ClaimsPrincipal(identity);
        }
    }
    else
    {
        context.Response.StatusCode = 401;
        context.Response.Headers.Set("WWW-Authenticate", "Basic realm=\"dotnetthoughts.net\"");
    }
    await _next(context);
}

Este código está escrito en una versión beta del núcleo de asp.net. Espero eso ayude.

Anuraj
fuente
1
¡Gracias por tu respuesta! Esto es exactamente lo que estaba buscando: una solución simple para la autenticación básica.
Félix
1
Hay un error en este código debido al uso de credentialstring.Split (':') - no manejará correctamente las contraseñas que contienen dos puntos. El código de la respuesta de Felix no sufre este problema.
Phil Dennis
111

Ahora, después de que me señalaron en la dirección correcta, aquí está mi solución completa:

Esta es la clase de middleware que se ejecuta en cada solicitud entrante y verifica si la solicitud tiene las credenciales correctas. Si no hay credenciales presentes o si son incorrectas, el servicio responde con un error 401 no autorizado de inmediato.

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        string authHeader = context.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.StartsWith("Basic"))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':');

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            if(username == "test" && password == "test" )
            {
                await _next.Invoke(context);
            }
            else
            {
                context.Response.StatusCode = 401; //Unauthorized
                return;
            }
        }
        else
        {
            // no authorization header
            context.Response.StatusCode = 401; //Unauthorized
            return;
        }
    }
}

La extensión de middleware debe llamarse en el método Configure de la clase de inicio del servicio

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

¡Y eso es todo! :)

Puede encontrar un muy buen recurso para middleware en .Net Core y autenticación aquí: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Felix
fuente
4
Gracias por publicar la solución completa. Sin embargo, tuve que agregar la línea 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' a la sección "encabezado sin autorización" para que el navegador solicite las credenciales.
m0n0ph0n
¿Hasta qué punto es segura esta autenticación? ¿Qué pasa si alguien olfatea el encabezado de la solicitud y obtiene el nombre de usuario / contraseña?
Bewar Salah
5
@BewarSalah debe servir este tipo de solución a través de https
wal
2
Algunos controladores deberían permitir archivos anónimos. Esta solución de middleware fallará en ese caso porque verificará el encabezado de autorización en cada solicitud.
Karthik
28

Para usar esto solo para controladores específicos, por ejemplo, use esto:

app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)), 
            builder =>
            {
                builder.UseMiddleware<AuthenticationMiddleware>();
            });
mr_squall
fuente
22

Creo que puedes ir con JWT (Json Web Tokens).

Primero necesitas instalar el paquete System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Deberá agregar un controlador para la generación y autenticación de tokens como este:

public class TokenController : Controller
{
    [Route("/token")]

    [HttpPost]
    public IActionResult Create(string username, string password)
    {
        if (IsValidUserAndPasswordCombination(username, password))
            return new ObjectResult(GenerateToken(username));
        return BadRequest();
    }

    private bool IsValidUserAndPasswordCombination(string username, string password)
    {
        return !string.IsNullOrEmpty(username) && username == password;
    }

    private string GenerateToken(string username)
    {
        var claims = new Claim[]
        {
            new Claim(ClaimTypes.Name, username),
            new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
            new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
        };

        var token = new JwtSecurityToken(
            new JwtHeader(new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                                         SecurityAlgorithms.HmacSha256)),
            new JwtPayload(claims));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Después de esa actualización, la clase Startup.cs se verá a continuación:

namespace WebAPISecurity
{   
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = "JwtBearer";
            options.DefaultChallengeScheme = "JwtBearer";
        })
        .AddJwtBearer("JwtBearer", jwtBearerOptions =>
        {
            jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                ValidateIssuer = false,
                //ValidIssuer = "The name of the issuer",
                ValidateAudience = false,
                //ValidAudience = "The name of the audience",
                ValidateLifetime = true, //validate the expiration and not before values in the token
                ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
            };
        });

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseAuthentication();

        app.UseMvc();
    }
}

Y eso es todo, lo que queda ahora es poner [Authorize]atributo en los Controladores o Acciones que desee.

Aquí hay un enlace de un tutorial completo y sencillo.

http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/

AJ -
fuente
9

Lo he implementado BasicAuthenticationHandlerpara la autenticación básica para que pueda usarlo con atributos estándar Authorizey AllowAnonymous.

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authHeader = (string)this.Request.Headers["Authorization"];

        if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':', StringComparison.OrdinalIgnoreCase);

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            //you also can use this.Context.Authentication here
            if (username == "test" && password == "test")
            {
                var user = new GenericPrincipal(new GenericIdentity("User"), null);
                var ticket = new AuthenticationTicket(user, new AuthenticationProperties(), Options.AuthenticationScheme);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            else
            {
                return Task.FromResult(AuthenticateResult.Fail("No valid user."));
            }
        }

        this.Response.Headers["WWW-Authenticate"]= "Basic realm=\"yourawesomesite.net\"";
        return Task.FromResult(AuthenticateResult.Fail("No credentials."));
    }
}

public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
    public BasicAuthenticationMiddleware(
       RequestDelegate next,
       IOptions<BasicAuthenticationOptions> options,
       ILoggerFactory loggerFactory,
       UrlEncoder encoder)
       : base(next, options, loggerFactory, encoder)
    {
    }

    protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
    {
        return new BasicAuthenticationHandler();
    }
}

public class BasicAuthenticationOptions : AuthenticationOptions
{
    public BasicAuthenticationOptions()
    {
        AuthenticationScheme = "Basic";
        AutomaticAuthenticate = true;
    }
}

Registro en Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. Con este código, puede restringir cualquier controlador con el atributo estándar Autorize:

[Authorize(ActiveAuthenticationSchemes = "Basic")]
[Route("api/[controller]")]
public class ValuesController : Controller

y use el atributo AllowAnonymoussi aplica el filtro de autorización en el nivel de la aplicación.

Ivan R.
fuente
1
Usé su código, pero noté que no importa si Authorize (ActiveAuthenticationSchemes = "Basic")] está configurado o no en cada llamada, el middleware se activa, lo que da como resultado que cada controlador se valide también cuando no se desea.
CSharper
Me gusta esta respuesta
KTOV
1
ejemplo de trabajo aquí: jasonwatmore.com/post/2018/09/08/…
bside
Creo que esta respuesta es el camino a seguir, ya que le permite usar los atributos estándar autorizar / permitir anónimos más arriba en la solución. Además de eso, debería ser fácil usar otro esquema de autenticación más adelante en la fase del proyecto si fuera necesario
Frederik Gheysels
0

En este repositorio público de Github https://github.com/boskjoett/BasicAuthWebApi , puede ver un ejemplo simple de una API web ASP.NET Core 2.2 con puntos finales protegidos por autenticación básica.

Bo Christian Skjøtt
fuente
Si desea utilizar la identidad autenticada en su controlador (SecureValuesController), crear un ticket no es suficiente ya que el objeto Request.User está vacío. ¿Todavía necesitamos asignar este ClaimsPrincipal al contexto actual en AuthenticationHandler? Así lo hicimos en WebApi más antiguo ...
pseabury
0

Como se dijo correctamente en publicaciones anteriores, una de las formas es implementar un middleware de autenticación básico personalizado. Encontré el mejor código de trabajo con explicación en este blog: autenticación básica con middleware personalizado

Remití el mismo blog pero tuve que hacer 2 adaptaciones:

  1. Mientras agrega el middleware en el archivo de inicio -> Configurar función, siempre agregue middleware personalizado antes de agregar app.UseMvc ().
  2. Mientras lee el nombre de usuario y la contraseña del archivo appsettings.json, agregue la propiedad estática de solo lectura en el archivo de inicio. Luego lea de appsettings.json. Finalmente, lea los valores desde cualquier lugar del proyecto. Ejemplo:

    public class Startup
    {
      public Startup(IConfiguration configuration)
      {
        Configuration = configuration;
      }
    
      public IConfiguration Configuration { get; }
      public static string UserNameFromAppSettings { get; private set; }
      public static string PasswordFromAppSettings { get; private set; }
    
      //set username and password from appsettings.json
      UserNameFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("UserName").Value;
      PasswordFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("Password").Value;
    }
    
Palash Roy
fuente
0

Puedes usar un ActionFilterAttribute

public class BasicAuthAttribute : ActionFilterAttribute
{
    public string BasicRealm { get; set; }
    protected NetworkCredential Nc { get; set; }

    public BasicAuthAttribute(string user,string pass)
    {
        this.Nc = new NetworkCredential(user,pass);
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.HttpContext.Request;
        var auth = req.Headers["Authorization"].ToString();
        if (!String.IsNullOrEmpty(auth))
        {
            var cred = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Substring(6)))
                .Split(':');
            var user = new {Name = cred[0], Pass = cred[1]};
            if (user.Name == Nc.UserName && user.Pass == Nc.Password) return;
        }

        filterContext.HttpContext.Response.Headers.Add("WWW-Authenticate",
            String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel"));
        filterContext.Result = new UnauthorizedResult();
    }
}

y agrega el atributo a tu controlador

[BasicAuth("USR", "MyPassword")]

Luca Ziegler
fuente