Autenticação de API da Web ASP.NET Core

96

Estou lutando para configurar a autenticação em meu serviço da web. O serviço é criado com a API da Web ASP.NET Core.

Todos os meus clientes (aplicativos WPF) devem usar as mesmas credenciais para chamar as operações de serviço da web.

Depois de alguma pesquisa, eu vim com a autenticação básica - enviar um nome de usuário e senha no cabeçalho da solicitação HTTP. Mas depois de horas de pesquisa, parece-me que a autenticação básica não é o caminho a percorrer no ASP.NET Core.

A maioria dos recursos que encontrei está implementando autenticação usando OAuth ou algum outro middleware. Mas isso parece ser superdimensionado para o meu cenário, além de usar a parte Identity do ASP.NET Core.

Então, qual é a maneira certa de atingir meu objetivo - autenticação simples com nome de usuário e senha em um serviço da Web ASP.NET Core?

Desde já, obrigado!

Felix
fonte

Respostas:

73

Você pode implementar um middleware que lida com a autenticação 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 foi escrito em uma versão beta do núcleo do asp.net. Espero que ajude.

Anuraj
fonte
1
Obrigado pela sua resposta! Isso é exatamente o que eu estava procurando - uma solução simples para autenticação básica.
Felix
1
Há um bug neste código devido ao uso de credentialstring.Split (':') - ele não lidará corretamente com senhas que contenham dois pontos. O código na resposta de Felix não sofre desse problema.
Phil Dennis
110

Agora, depois que fui apontado na direção certa, aqui está minha solução completa:

Esta é a classe de middleware que é executada em cada solicitação de entrada e verifica se a solicitação tem as credenciais corretas. Se nenhuma credencial estiver presente ou se estiverem erradas, o serviço responde com um erro 401 Não autorizado imediatamente.

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

A extensão de middleware precisa ser chamada no método Configure da classe de inicialização do serviço

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

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

E isso é tudo! :)

Um recurso muito bom para middleware em .Net Core e autenticação pode ser encontrado aqui: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Felix
fonte
4
Obrigado por postar a solução completa. No entanto, eu tive que adicionar a linha 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' para a seção 'sem cabeçalho de autorização' para que o navegador solicite credenciais.
m0n0ph0n
Quanto essa autenticação é segura? E se alguém farejar o cabeçalho da solicitação e obter o nome de usuário / senha?
Bewar Salah
5
@BewarSalah você deve servir este tipo de solução por https
wal
2
Alguns controladores devem permitir o anônimo. Essa solução de middleware falhará nesse caso porque verificará o cabeçalho de autorização em cada solicitação.
Karthik
28

Para usar isso apenas para controladores específicos, por exemplo, use:

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

Eu acho que você pode ir com JWT (Json Web Tokens).

Primeiro você precisa instalar o pacote System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Você precisará adicionar um controlador para geração de token e autenticação 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);
    }
}

Depois disso, atualize a classe Startup.cs para ficar assim:

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

E é isso, o que resta agora é colocar o [Authorize]atributo nos Controladores ou Ações que você deseja.

Aqui está um link para um tutorial completo e direto.

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

AJ -
fonte
9

Eu implementei BasicAuthenticationHandler para autenticação básica para que você possa usá-la com atributos padrão Authorizee 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 em Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>(); . Com este código, você pode restringir qualquer controlador com o atributo standart Autorize:

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

e use o atributo AllowAnonymousse você aplicar o filtro de autorização no nível do aplicativo.

Ivan R.
fonte
1
Usei seu código, mas notei que não importa se o Authorize (ActiveAuthenticationSchemes = "Basic")] está definido ou não em todas as chamadas o middleware é ativado resultando em cada controlador validado também quando não é desejado.
CSharper
Gosto desta resposta
KTOV
1
exemplo de trabalho aqui: jasonwatmore.com/post/2018/09/08/…
bside
Eu acho que essa é a resposta é o caminho a percorrer, pois permite que você use os atributos padrão de autorização / permissão anônima mais adiante na solução. Além disso, deve ser fácil usar outro esquema de autenticação posteriormente na fase do projeto, caso seja necessário
Frederik Gheysels
0

Neste repositório público do Github https://github.com/boskjoett/BasicAuthWebApi, você pode ver um exemplo simples de uma API da web ASP.NET Core 2.2 com pontos de extremidade protegidos por autenticação básica.

Bo Christian Skjøtt
fonte
Se você deseja usar a Identidade Autenticada em seu controlador (SecureValuesController), criar um ticket não é suficiente, pois o objeto Request.User está vazio. Ainda precisamos atribuir este ClaimsPrincipal ao contexto atual no AuthenticationHandler? É assim que fazíamos no WebApi antigo ...
pseabury
0

Como corretamente dito em posts anteriores, uma das maneiras é implementar um middleware de autenticação básica customizado. Eu encontrei o melhor código funcional com explicação neste blog: Basic Auth with custom middleware

Referi o mesmo blog, mas tive que fazer 2 adaptações:

  1. Ao adicionar o middleware no arquivo de inicialização -> função Configurar, sempre adicione o middleware personalizado antes de adicionar app.UseMvc ().
  2. Ao ler o nome de usuário e a senha do arquivo appsettings.json, adicione a propriedade estática somente leitura no arquivo de inicialização. Em seguida, leia appsettings.json. Finalmente, leia os valores de qualquer lugar no projeto. Exemplo:

    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
fonte
0

Você pode usar um 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();
    }
}

e adicione o atributo ao seu controlador

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

Luca Ziegler
fonte