Search code examples
asp.net-coreasp.net-web-apijwtasp.net-core-mvcaccess-token

I can't authorize with the token in local storage. Request to the API returns 200, but navigating to [Authorize] pages yields 302


Web API Enpoint

[HttpPost("login")]
        public async Task<IActionResult> Login([FromBody] LoginRequestDto loginRequest)
        {
            var token = await _authService.LoginAsync(loginRequest.UsernameOrEmail, loginRequest.Password, 900);
            return Ok(new { AccessToken = token.AccessToken, Expiration = token.Expiration, RefreshToken = token.RefreshToken });
        }

The json that the api returns

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsIm5iZiI6MTcxMjA1NDcwNSwiZXhwIjoxNzEyMDU1NjA1LCJpc3MiOiJ3d3cubXlhcGkuY29tIiwiYXVkIjoid3d3LmJpbG1lbW5lLmNvbSJ9.O8cY-lBV-uWNPo4R9TJEdmzV4R7TbCja3N7pslU5WRQ",
  "expiration": "2024-04-02T11:00:05Z",
  "refreshToken": "JBKlltji4txzErH660t+lydjEhxCKJbP0HlEbakxFJM="
}

MVC Enpoint - Request discarded enpoint


        [HttpPost]
        public async Task<IActionResult> Login(LoginWebDto model)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5026/api/Auth/login");

            var loginData = new
            {
                UsernameOrEmail = model.UsernameOrEmail,
                Password = model.Password
            };

            request.Content = new StringContent(JsonConvert.SerializeObject(loginData), System.Text.Encoding.UTF8,
                "application/json");

            var client = _httpClientFactory.CreateClient();

            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                var tokenResponse = JsonConvert.DeserializeObject<TokenResponseDto>(content);
                
                var handler = new JwtSecurityTokenHandler();
                var token = handler.ReadJwtToken(tokenResponse.AccessToken);
                _httpContextAccessor.HttpContext.Session.SetString("AccessToken", tokenResponse.AccessToken);
                _httpContextAccessor.HttpContext.Session.SetString("RefreshToken", tokenResponse.RefreshToken);

                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Login failed. Incorrect username or password.");
                return View(model);
            }
        }

Login.cshtml View

@model WebMVC.DTOs.Login.LoginWebDto
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
<h1>Login</h1>

@using (Html.BeginForm("Login", "Account", FormMethod.Post, new { @class = "login-form", id = "login-form" }))
{
    @Html.AntiForgeryToken()

    <div>
        <label asp-for="UsernameOrEmail">Username or Email:</label>
        <input value="admin" asp-for="UsernameOrEmail" />
        <span asp-validation-for="UsernameOrEmail"></span>
    </div>

    <div>
        <label asp-for="Password">Password:</label>
        <input value="Admin1." asp-for="Password" type="password" />
        <span asp-validation-for="Password"></span>
    </div>

    <button type="submit">Login</button>
}

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<script>
    $(document).ready(function() {
        $('#login-form').submit(function(event) {
            var formData = {
                UsernameOrEmail: $('input[name="UsernameOrEmail"]').val(),
                Password: $('input[name="Password"]').val()
            };
    
            $.ajax({
                type: 'POST',
                url: 'http://localhost:5026/api/Auth/login',
                data: JSON.stringify(formData),
                contentType: 'application/json',
                success: function(response) {
                    localStorage.setItem('accessToken', response.accessToken);
                    localStorage.setItem('refreshToken', response.refreshToken);
                    window.location.href = '/Home/Index';
                },
                error: function(xhr, status, error) {
                    console.error('Login failed:', error);
                    alert('Login failed. Incorrect username or password.');
                }
            });
    
            return true;
        });
        $.ajaxSetup({
            beforeSend: function(xhr) {
                var accessToken = localStorage.getItem('accessToken');
                if (accessToken) {
                    xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
                }
            }
        });
    })
</script>

</body>
</html>

API Program.cs

using System.Configuration;
using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using Application.Abstractions.Services;
using Domain.Entities.Identity;
using Infrastructure;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Persistence;
using Persistence.Context;
using Persistence.Services;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors(options => options.AddDefaultPolicy(policy =>
    policy.WithOrigins("http://localhost:5273", "https://localhost:5273").AllowAnyHeader().AllowAnyMethod()
        .AllowCredentials()
));
builder.Services.AddSession();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddControllersWithViews()
    .AddJsonOptions(opt => opt.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
builder.Services.AddIdentity<AppUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddInfrastructureServices();
builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidAudience = builder.Configuration["Token:Audience"],
            ValidIssuer = builder.Configuration["Token:Issuer"],
            IssuerSigningKey =
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Token:SecurityKey"])),
            LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
                expires != null ? expires > DateTime.UtcNow : false,

            NameClaimType = ClaimTypes.Name,
            RoleClaimType = ClaimTypes.Role,
        };
    });
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Admin"));
});
builder.Services.AddSwaggerGen(c =>
{
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description =
            "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer eyJhbGciOiJIU125InR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1Law0aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI52MzA3ZGNkLTUyZWItNDAwZi04NWJlLTI3MGIxNWUwZjRlYiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiJhZG1pbjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1L124kZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJhZG1pbkBleGFtcGxlLmNvbSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNzA4MTcxNDUxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMjYiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMjYifQ.G0aHe5M_D7QOEYnidH-s7Cf48Ftf512sEUCyLbIN\"",
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] { }
        }
    });
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var userManager = services.GetRequiredService<UserManager<AppUser>>();
    var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
    SeedData.Initialize(services, userManager, roleManager).Wait();
    services.GetRequiredService<ILogger<Program>>();
}

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors();
app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllers();

app.Run();

MVC Program.cs

using Domain.Entities.Identity;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Persistence.Context;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddSession();
builder.Services.AddHttpClient();
builder.Services.AddControllersWithViews();
builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<AppUser, IdentityRole>(options =>
    {
        options.User.RequireUniqueEmail = true;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

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

app.UseRouting();

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

app.UseSession();

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

app.Run();

I am encountering problems when authorising with the token in local storage. The request to the API endpoint returns a status code of 200, but when navigating to pages labelled [Authorize] it returns a status code of 302.


Solution

  • Your API project uses JWT for verification, while the MVC project uses Identity cookie verification. When you access the Authorized page in MVC without logging in, you will be redirected to the login page for verification, so 302 occurs.

    In the code you provided, it seems that there is a generated token method in the API project, and the relevant token authentication is configured. In your view, access the login method through form submission to invoke Login, and process the login logic: create an httpClientFactory instance, go to the API to get the token, save it in the session, and return home/index successfully, but fail. Then return to the current page. During the verification process, if you want to verify the token, you should get the token from the session in the mvc project to verify:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
    
                var accessToken = context.HttpContext.Session.GetString("AccessToken");
    
                context.Token = accessToken;
    
                return Task.CompletedTask;
            }
        };
        options.SaveToken = true;
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = configuration["JWT:ValidAudience"],
            ValidIssuer = configuration["JWT:ValidIssuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
        };
    });