23
respostas

[Dúvida] Erro de autorização ao consumir uma API que possui autenticação. Não consigo colocar o token no cabeçalho da requisição

Bom dia pessoal. Estou com um problema referente a aplicação MVC que consome uma API que possui autenticação JWT bearer. A API funciona normalmente. No entanto, ao consumi-la, eu recebo o erro 401 no navegador, como podem ver abaixo:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Inspecionei a página e me deparei com esse erro:

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Acredito que seja algo relacionada a configuração de cors, mas não sei exatamente o que é, segue o código da classe program.cs do .NET core 6:

//...
var secretKey = builder.Configuration.GetSection("AppSettings:SecretKey").Value;
var audience = "CreativeService";
builder.Services.AddSingleton(new TokenService(secretKey, audience));
// Converte a chave secreta lida em bytes
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var chaveCriptografada = new SymmetricSecurityKey(keyBytes);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = true,
            ValidAudience = "CreativeService",
            ValidateLifetime = true,
            IssuerSigningKey = chaveCriptografada,
        };
    });

//builder.Services.AddCors(options =>
//{
//    options.AddPolicy("corspolicy", builder =>
//    {
//        builder.WithOrigins("")
//               .AllowAnyMethod()
//               .AllowAnyHeader();
//    });
//});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}


app.UseAuthentication();

app.UseCors(builder => builder
.SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());

app.UseAuthorization();

//app.UseCors("corspolicy");


app.UseHttpsRedirection();

app.UseSession();

app.MapControllers();

app.Run();

Percebam que há códigos de configuração de cors comentados, o que significa que eu tentei configurar o cors de várias maneiras e nenhuma funcionou. Agora seguem o código do MVC:

//Program.cs do mvc:

using CreativeMultiCoisasMVC.Repositories.Interfaces;
using CreativeMultiCoisasMVC.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.AddTransient<IProdutoRepository, ProdutoRepository>();

builder.Services.AddHttpClient();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "JwtBearer"; // Define o esquema de desafio para JwtBearer
})
    .AddCookie("Cookies", options =>
    {
        options.LoginPath = "/Account/Login";
    })
    .AddJwtBearer("JwtBearer", options =>
    {
        options.Authority = "https://localhost:7035"; 
        options.Audience = "CreativeService"; 
    });


builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(5); 
    
});


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseSession();

app.UseEndpoints(endpoints =>

app.UseEndpoints(endpoints =>
{
    //endpoints.MapControllerRoute(
    //     name: "areas",
    //     pattern: "{area:exists}/{controller=Admin}/{action=Index}/{id?}"
    // );

    endpoints.MapControllerRoute(
        name: "categoriaFiltro",
        pattern: "Produto/{action}/{categoria?}",
        defaults: new { Controller = "Produto", action = "List" });


    endpoints.MapControllerRoute(
     name: "default",
     pattern: "{controller=Home}/{action=Index}/{id?}");
}));


app.Run();

É isso e espero que alguém consiga me ajudar. Desde já agradeço

23 respostas

Oii, Luis! Tudo bem?

O erro 401 é comum acontecer quando a autenticação falha. Isso pode acontecer se o token JWT não for enviado corretamente no cabeçalho da solicitação ou se o servidor rejeitar o token por algum motivo (por exemplo, se o token estiver expirado ou se a assinatura não corresponder). Então, podemos considerar que o problema esteja na autenticação JWT e não com o CORS.

Pelo código disponibilizado, você configurou a autenticação JWT no servidor e no cliente, porém, você precisa garantir que o token JWT seja enviado corretamente em todas as solicitações para a API.

No lado do cliente (MVC), sugiro adicionar o token JWT ao cabeçalho Authorization de todas as solicitações para a API. Uma forma de fazer isso é usar um HttpClient e configurando o cabeçalho Authorization antes de fazer a solicitação. Segue o link da documentação da Microsoft que possui informações aprofundadas sobre o HttpClient e serve como um apoio nos estudos:

E no lado do servidor é preciso garantir que o mesmo esteja configurado para aceitar e validar o token JWT, você já o fez certinho. Além disso, é preciso garantir que a chave secreta e o público-alvo (audience) correspondam ao que você está usando para criar o token JWT no lado do cliente.

Portanto, você está no caminho certo no seu código! Lembre-se que as sugestões acima devem ser testadas e aplicadas conforme as necessidades do seu projeto.

Qualquer dúvida, compartilhe aqui no fórum.

Bons estudos, Luis!

Oi NATHALIA, então...

Eu havia feito algumas alterações no código para consertar o erro mas não funcionou. Veja o código com a versão atualizada:

Aplicação MVC:

using CreativeMultiCoisasMVC.Repositories.Interfaces;
using CreativeMultiCoisasMVC.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;


var builder = WebApplication.CreateBuilder(args);


builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidAudience = "CreativeService",
            
        };
    });

// Add services to the container.


builder.Services.AddControllersWithViews();

builder.Services.AddTransient<IProdutoRepository, ProdutoRepository>();


builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(5); 
    
});



builder.Services.AddHttpContextAccessor();
builder.Services.AddLogging();

builder.Services.AddHttpClient();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");

    app.UseHsts();
}



app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseSession();

app.UseEndpoints(endpoints =>

app.UseEndpoints(endpoints =>
{
    //endpoints.MapControllerRoute(
    //     name: "areas",
    //     pattern: "{area:exists}/{controller=Admin}/{action=Index}/{id?}"
    // );

    endpoints.MapControllerRoute(
        name: "categoriaFiltro",
        pattern: "Produto/{action}/{categoria?}",
        defaults: new { Controller = "Produto", action = "List" });


    endpoints.MapControllerRoute(
     name: "default",
     pattern: "{controller=Home}/{action=Index}/{id?}");
}));


app.Run();
public class ProdutoController : Controller
{
    private readonly HttpClient _httpClient;
    private readonly IProdutoRepository _produtoRepository;
    public ProdutoController(HttpClient httpClient, IProdutoRepository produtoRepository)
    {
        _httpClient = httpClient;
        _produtoRepository = produtoRepository;
    }

    
    [Authorize]
    public async Task<IActionResult> List(string categoria)
    {
        IEnumerable<ProdutoViewModel> produtos;
        string categoriaAtual = string.IsNullOrEmpty(categoria) ? "Todos os produtos" : categoria;
        string descricaoCategoriaAtual = string.Empty;

        try
        {
            
            produtos = await _produtoRepository.GetProdutosAsync();

            if (!string.IsNullOrEmpty(categoria))
            {
                produtos = await _produtoRepository.GetProdutosPorCategoriaAsync(categoria);

                // Obtém a descrição da categoria a partir da resposta da API
                var categoriaResponse = await _httpClient
                    .GetAsync($"api/categoria/searchCategoria?nome={categoria}"); // Use o caminho relativo

                if (categoriaResponse.IsSuccessStatusCode)
                {
                    var categoriaData = await categoriaResponse.Content.ReadFromJsonAsync<CategoriaViewModel>();
                    descricaoCategoriaAtual = categoriaData?.Descricao;
                }
            }
        }
        catch
        {
            produtos = new List<ProdutoViewModel>();
        }

        var produtoViewModel = new ProdutoListViewModel
        {
            Produtos = produtos,
            CategoriaAtual = categoriaAtual,
            DescricaoCategoriaAtual = descricaoCategoriaAtual
        };

        return View(produtoViewModel);
    }
    //...
}

Controlador account do MVC:

public class AccountController : Controller
{
    private readonly IConfiguration _configuration;
    private readonly HttpClient _httpClient;

    public AccountController(HttpClient httpClient, IConfiguration configuration)
    {
        _configuration = configuration;
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri(_configuration["ApiBaseUrl"]);
    }


    [AllowAnonymous]
    [HttpGet]
    public IActionResult Register(string returnUrl)
    {
        return View(new RegisterViewModel()
        {
            ReturnUrl = returnUrl
        });
    }

    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            // Crie um objeto JSON para registro
            var registerJson = new
            {
                emailRegister = model.EmailRegister,
                userName = model.UserName,
                password = model.Password,
                passwordConfirm = model.PasswordConfirm,
                returnUrl = model.ReturnUrl
            };

            
            var jsonContent = JsonConvert.SerializeObject(registerJson);

            
            var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

            // Faça a solicitação HTTP POST para a URL de registro (usando a URL base)
            var response = await _httpClient.PostAsync("/api/account/register", content);

            if (response.IsSuccessStatusCode)
            {
                
                return RedirectToAction("Login", "Account");
            }
            else
            {
                
                ModelState.AddModelError("", "Erro no registro. Verifique os dados e tente novamente.");
            }
        }

        return View(model);
    }


    [AllowAnonymous]
    [HttpGet]
    public IActionResult Login(string returnUrl)
    {
        return View(new LoginViewModel() { ReturnUrl = returnUrl });
    }

    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (ModelState.IsValid)
        {
            var token = await GetApiAuthTokenAsync(model);

            if (!string.IsNullOrEmpty(token))
            {
                //Armazena o tokrn na sessão
                HttpContext.Session.SetString("AuthToken", token);

                // Adicione o token de autorização ao HttpClient
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

                return RedirectToAction("List", "Produto");
            }
            else
            {
                ModelState.AddModelError("", "Credenciais inválidas!");
            }
        }

        return View(model);
    }

    public async Task<string> GetApiAuthTokenAsync(LoginViewModel model)
    {
        var loginRequest = new
        {
            userName = model.UserName,
            password = model.Password,
        };

        var token = await SendPostRequest("/api/account/login", loginRequest);

        return token;
    }

    private async Task<string> SendPostRequest(string requestUri, object requestData)
    {
        var jsonContent = JsonConvert.SerializeObject(requestData);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync(requestUri, content);

        if (response.IsSuccessStatusCode)
        {
            return await response.Content.ReadAsStringAsync();
        }
        else
        {
            return null;
        }
    }
}

Projeto da API:

using System.Text;
using CreativeMultiCoisas.Context;
using CreativeMultiCoisas.Models;
using CreativeMultiCoisas.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
       options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));


builder.Services.AddIdentity<IdentityUser, IdentityRole>()
       .AddEntityFrameworkStores<AppDbContext>()
       .AddDefaultTokenProviders();

builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddScoped(sp => CarrinhoCompra.GetCarrinho(sp));

builder.Services.AddCors();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(4);

});



builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var secretKey = builder.Configuration.GetSection("AppSettings:SecretKey").Value;
var audience = "CreativeService";
builder.Services.AddSingleton(new TokenService(secretKey, audience));
// Converte a chave secreta lida em bytes
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var chaveCriptografada = new SymmetricSecurityKey(keyBytes);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = chaveCriptografada,
            ValidAudience = "CreativeService",
        };
    });



var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}


app.UseAuthentication();

app.UseAuthorization();

app.UseCors(c =>
{
    c.AllowAnyHeader();
    c.AllowAnyMethod();
    c.AllowAnyOrigin();
});


app.UseHttpsRedirection();

app.UseSession();

app.MapControllers();

app.Run();

Controlador account da API:

[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly SignInManager<IdentityUser> _signInManager;
    private readonly TokenService _tokenService;

    public AccountController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, TokenService tokenService)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _tokenService = tokenService;
    }


    [AllowAnonymous]
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] RegisterDTO model)
    {
        if (ModelState.IsValid)
        {
            var existingUser = await _userManager.FindByNameAsync(model.UserName);
            if (existingUser != null)
            {
                return BadRequest("Nome de usuário já está em uso.");
            }

            existingUser = await _userManager.FindByEmailAsync(model.EmailRegister);
            if (existingUser != null)
            {
                return BadRequest("Endereço de e-mail já está em uso.");
            }

            var user = new IdentityUser { UserName = model.UserName, Email = model.EmailRegister };
            var result = await _userManager.CreateAsync(user, model.Password);

            if (result.Succeeded)
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return Ok("Registro bem-sucedido.");
            }
            else
            {
                return BadRequest(result.Errors);
            }
        }

        return BadRequest(ModelState);
    }

    [AllowAnonymous]
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDTO model)
    {
        if (ModelState.IsValid)
        {
            var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, isPersistent: false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                // Gere um token JWT usando o serviço
                var token = _tokenService.GenerateToken(model.UserName, _tokenService.SecretKey, 4);
                
                HttpContext.Response.Headers.Add("Authorization", $"Bearer {token}");

                //return Ok(new
                //{
                //    Token = token,
                //    Message = "Login bem-sucedido."
                //});

                return Ok(token);
            } 
            else
            {
                return BadRequest("Login falhou. Verifique suas credenciais.");
            }
        }

        return BadRequest(ModelState);
    }


    [Authorize(AuthenticationSchemes = "Bearer")]
    [HttpPost("logout")]
    public async Task<IActionResult> Logout()
    {
        HttpContext.Session.Clear();
        HttpContext.User = null;
        await _signInManager.SignOutAsync();
        return Ok("Logout bem-sucedido.");
    }
}

Controlador produto da API:

[Route("api/[controller]")]
[ApiController]
public class ProdutoController : ControllerBase
{
    private readonly AppDbContext _context;

    public ProdutoController(AppDbContext context)
    {
        _context = context;
    }

    [Authorize(AuthenticationSchemes = "Bearer")]
    [HttpGet("obterProduto")]
    public async Task<ActionResult<IEnumerable<ProdutoDTO>>> GetProdutos()
    {
        var produtos = await _context.Produtos
            .Include(p => p.Categoria)
            .Select(p => new ProdutoDTO
            {
                ProdutoId = p.ProdutoId,
                Nome = p.Nome,
                DescricaoCurta = p.DescricaoCurta,
                DescricaoDetalhada = p.DescricaoDetalhada,
                Preco = p.Preco,
                ImagemUrl = p.ImagemUrl,
                ImagemThumbnailUrl = p.ImagemThumbnailUrl,
                IsPromocao = p.IsPromocao,
                EmEstoque = p.EmEstoque,
                CategoriaDTO = new CategoriaDTO
                {
                    CategoriaId = p.CategoriaId,
                    CategoriaNome = p.Categoria.CategoriaNome,
                    Descricao = p.Categoria.Descricao
                },
            })
            .ToListAsync();

        return produtos;
    }
    //...

Classe token service da API:

` using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text;

namespace CreativeMultiCoisas.Services;

public class TokenService { public string SecretKey { get; private set; } public string Audience { get; private set; }

public TokenService(string secretKey, string audience)
{
    SecretKey = secretKey;
    Audience = audience; 
}

public string GenerateToken(string userName, string secretKey, int expirationMinutes)
{
    var key = Encoding.UTF8.GetBytes(secretKey);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, userName),
        }),
        Expires = DateTime.UtcNow.AddMinutes(expirationMinutes),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
        Audience = Audience, 
    };
    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

}

`

Bom dia Luis, tudo bom?

Por favor tente adicionar a anotação a seguir na sua API:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ProdutoController : ControllerBase
{
  // ...

Ou altere onde vc já está usando para:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("obterProduto")]
    public async Task<ActionResult<IEnumerable<ProdutoDTO>>> GetProdutos()
    {
        // ...

Se for possível, mande para nós o link do repositório git da sua solução, para podermos ajudar melhor!

Espero que ajude! De qualquer forma no aguardo!

Olá André.

Segue os links:

Link da API:

Link do projeto MVC:

Boa tarde Luis,

Vou começar a olhar aqui, assim que encontrar o que pode estar acontecendo te retorno neste post.

Ok, muito obrigado

Olá, Luis! Tudo beleza camarada?

Atualizando o status. Debugando encontrei que o token que está sendo gerado está inválido. Insira aqui a descrição dessa imagem para ajudar na acessibilidade

A configuração do Cors está OK aqui.

Ah entendi.

Mas por que ele é válido na API e inválido no lado cliente?

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

André, perdão. Eu havia esquecido de commitar as alterações no git hub. Eu já tinha resolvido essa questão do token inválido. O problema mesmo está na hora de inseri-lo no cabeçalho da requisição.

Clique no link novamente por gentileza e verá que eu já commitei as alterações. Os commits respectivamente são:

Alteracoes no lado cliente

Alterações na API.

Aguardo o seu retorno. Já estou a bastante tempo tentando resolver essa questão do cabeçalho. Além disso, percebi que ele mostra o horário que foi feito o login 3 horas a frente, ou seja, se o login foi feita as 15:37, ele está exibindo que foi feito as 18:37. Percebi isso colocando o breakpoint na aplicação.

Luis, bom dia!

Beleza camarada, vou atualizar o projeto aqui e continuar a verificar.

Ok, no aguado e obrigado

Boa noite mestre Luis,

Fiz um ajuste no código do List para recuperar o Token, para ser utilizado na requisição de produtos, utilizando uma construção que você já tinha feito.:

public async Task<IActionResult> List(string categoria)
    {

        var token = Request.Cookies["AuthToken"];
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        var response = await _httpClient.GetAsync("https://localhost:7035/api/produto/obterproduto");


        if (response.IsSuccessStatusCode)
        {
            string content = await response.Content.ReadAsStringAsync();
            var lista = JsonConvert.DeserializeObject<List<ProdutoViewModel>>(content);

            ViewBag.Produtos = lista;
        }

        IEnumerable<ProdutoViewModel> produtos;
        string categoriaAtual = string.IsNullOrEmpty(categoria) ? "Todos os produtos" : categoria;
        string descricaoCategoriaAtual = string.Empty;

        try
        {
            
            produtos = await _produtoRepository.GetProdutosAsync();

           
        }
        catch
        {
            produtos = new List<ProdutoViewModel>();
        }

        var produtoViewModel = new ProdutoListViewModel
        {
            Produtos = produtos,
            CategoriaAtual = categoriaAtual,
            DescricaoCategoriaAtual = descricaoCategoriaAtual
        };

        return View(produtoViewModel);
    }

André, o código não funcionou. O mesmo erro persiste

Boa tarde Luis,

Nesta Action do controlador eu removi o [Autorize], pelo fato de estar validando na API. Ai o método de login eu mantenho o código que vc disponibilizou no Git.

Então André, não faz sentido. Se tirar o atributo authorize do lado cliente, é possível acessar a área reestrita através da URL

Realmente hehehe, eu foquei em "bater" na API, por estar levando na requisição o token jwt.

Sugiro você dar uma olhada no meu arquivo appsettings.json. Perceba que defini a URL base ali.

{

  "ApiBaseUrl": "https://localhost:7035", //URL base

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Além disso, verifique a interface e classe, respectivamente IProdutoRepository e ProdutoRepository só pra você ter uma noção da abordagem que eu estou usando.

Aproveita também, coloca o breakpoint na aplicação. E veja os horários de login. Estão sendo exibidos com 3 horas a frente