403 Forbidden Error while trying to access authorized routes in a .NetCore 8 WebAPI -

P. G. Choudhury 61 Reputation points
2024-11-05T18:16:25.4366667+00:00

Hi All,

Let me discuss my problem in clear detail. I am trying to build a backend webAPI for an app using .NetCore 8. I created the database for my webapi by using code-first migrations on AspNet Identity. The respective tables have been created. In the AspNetRoles table, I have seeded 4 roles during migration, Actor, Painter, Photographer and Musician. I have activated and configured Identity, with JWT and Swagger for writing and testing different type of controller endpoints. I have an Auth controller, with Register and Login action methods. I am able to create new user and also save roles from swagger in the tables AspNetUsers and AspNetUserRoles. No problem there.

Now, I have created a protected route in a different api controller, which is to be accessed by Actor only. I am using swagger to run the Login method and get a JWT token for a user with Actor role from Auth controller and then setting that token in the Swagger Authorize configuration popup. But when I am executing the call, instead of being able to access the protected resource I am getting a 403 Forbidden Response! This is where I am stuck. I'll post some relevant code blocks from my project, so that it's easier for you.

Program.cs -

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddSwaggerExplorer()
                .InjectDbContext(builder.Configuration)
                .AddAppConfig(builder.Configuration)
                .AddIdentityHandlersAndStores()
                .AddIdentityAuth(builder.Configuration);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
var app = builder.Build();
app.ConfigureSwaggerExplorer()
   .ConfigureCORS(builder.Configuration)
   .AddIdentityAuthMiddlewares();
app.UseDefaultFiles();
app.UseStaticFiles();
// Configure the HTTP request pipeline.
app.MapControllers();
app.Run();

IdentityExtensions.cs -

public static class IdentityExtensions
{
    public static IServiceCollection AddIdentityHandlersAndStores(this IServiceCollection services)
    {
        services.AddIdentity<AppUser, IdentityRole>()
                .AddEntityFrameworkStores<ForSideDBContext>()
                .AddDefaultTokenProviders();
        return services;
    
    }
    public static IServiceCollection AddIdentityAuth(this IServiceCollection services, IConfiguration config)
    {
        var authSetting = config.GetSection("AuthSettings");
        services.AddAuthentication(opt =>
        {
            opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(y =>
        {
            y.RequireHttpsMetadata = false;
            y.SaveToken = true;
            y.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AuthSettings:JWTSecret").Value!)),
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidAudience = authSetting["validAudience"],
                ValidIssuer = authSetting["validIssuer"]
            };
        });
        services.AddAuthorization(options =>
        {
            options.FallbackPolicy = new AuthorizationPolicyBuilder()
                                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                                    .RequireAuthenticatedUser()
                                    .Build();
        });
        return services;
    }
    public static WebApplication AddIdentityAuthMiddlewares(this WebApplication app)
    {
        app.UseAuthentication();
        app.UseAuthorization();
        return app;
    }
}

SwaggerExtensions.cs -

public static class SwaggerExtensions
{
    public static IServiceCollection AddSwaggerExplorer(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen(op =>
        {
            op.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description = "Provide JWT token here",
                Name = "Authorization Settings",
                In = ParameterLocation.Header,
                BearerFormat = "JWT",
                Type = SecuritySchemeType.Http,
                Scheme = "Bearer"
            });
            op.AddSecurityRequirement(new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,
                            Id = "Bearer"
                        },
                        Scheme = "oauth2",
                        Name = "Bearer",
                        In = ParameterLocation.Header
                    },
                    new List<string>()
                },
            });
        });
        return services;
    }
    public static WebApplication ConfigureSwaggerExplorer(this WebApplication app)
    {
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }
        return app;
    }
}

AuthController.cs -

[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
    private readonly UserManager<AppUser> _userManager;
    private readonly IConfiguration _configuration;
    public AuthController(UserManager<AppUser> userManager, IConfiguration configuration)
    {
        _userManager = userManager;
        _configuration = configuration;
    }
    [HttpPost("register")]
    [AllowAnonymous]
    public async Task<ActionResult<string>> Register(RegisterViewModel viewModel)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        AppUser user = new AppUser
        {
            Email = viewModel.Email,
            FullName = viewModel.FullName,
            MobileNumber=viewModel.MobileNumber,
            SubscriptionType = viewModel.SubscriptionType,
            UserName = viewModel.Email
        };
        IdentityResult result = await _userManager.CreateAsync(user, viewModel.Password);
        if (result.Succeeded)
        {
            IdentityResult roleResult = await _userManager.AddToRoleAsync(user, viewModel.Role);
            if (roleResult.Succeeded)
            {
                return Ok(new AuthResponseViewModel
                {
                    Result = true,
                    Message = "Accout created and role saved to database!"
                });
            }
            else
            {
                return Ok(new AuthResponseViewModel
                {
                    Result = true,
                    Message = "Account created but problem saving role to database!"
                });
            }
        }
        else
        {
            return BadRequest(result.Errors);
        }
    }
    [HttpPost("login")]
    [AllowAnonymous]
    public async Task<ActionResult<AuthResponseViewModel>> Login(LoginViewModel loginViewModel)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            AppUser? user = await _userManager.FindByEmailAsync(loginViewModel.Email);
            if (user == null)
            {
                return Unauthorized(new AuthResponseViewModel
                {
                    Result = false,
                    Message = "Invalid username and password combination",
                    Token = ""
                });
            }
            else
            {
                bool result = await _userManager.CheckPasswordAsync(user, loginViewModel.Password);
                if (!result)
                {
                    return Unauthorized(new AuthResponseViewModel
                    {
                        Result = false,
                        Message = "Invalid username and password combination"
                    });
                }
                else
                {
                    string token = CreateToken(user);
                    return Ok(new AuthResponseViewModel
                    {
                        Token = token,
                        Result = true,
                        Message = "Login was successful"
                    });
                }
            }
        }
    }
    private string CreateToken(AppUser user)
    {
        IConfigurationSection? authSettings = _configuration.GetSection("AuthSettings");
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        byte[] securityKey = Encoding.ASCII.GetBytes(authSettings.GetSection("JWTSecret").Value!);
        List<Claim> claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Aud, authSettings.GetSection("validAudience").Value!),
            new Claim(JwtRegisteredClaimNames.Iss, authSettings.GetSection("validIssuer").Value!),
            new Claim(JwtRegisteredClaimNames.Email, user.Email ?? ""),
            new Claim(JwtRegisteredClaimNames.Name, user.FullName ?? ""),
            new Claim(JwtRegisteredClaimNames.NameId, user.Id ?? "")
        };
        SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddDays(1),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(securityKey),
                SecurityAlgorithms.HmacSha256
            )
        };
        SecurityToken token = tokenHandler.CreateToken(descriptor);
        return tokenHandler.WriteToken(token);
    }
}

LoginViewModel.cs -

public class LoginViewModel
{
    [Required]
    public string Email { get; set; } = string.Empty;
    [Required]
    public string Password { get; set; } = string.Empty;
}

RegisterViewModel.cs -

public class RegisterViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
    [Required]
    public string FullName { get; set; } = string.Empty;
    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; } = string.Empty;
    [Required]
    [DataType(DataType.Password)]
    public string ConfirmPassword { get; set; } = string.Empty;
    [Required]
    public string MobileNumber { get; set; } = string.Empty;
    [Required]
    public string SubscriptionType { get; set; } = string.Empty;
    [Required]
    public string Role { get; set; } = string.Empty;
}

AuthResponseViewModel.cs -

public class AuthResponseViewModel
{
    public string Token { get; set; } = string.Empty;
    public bool Result { get; set; }
    public string Message { get; set; } = string.Empty;
}

AuthSettings.cs -

public class AuthSettings
{
    public string JWTSecret { get; set; }
}

ResourceController.cs -

[ApiController]
[Route("[controller]")]
public class ResourceController : ControllerBase
{
	[HttpGet("actors")]
    [Authorize(Roles = "Actor")]
    public string ForActorsOnly()
    {
       return "This resource is to be accessed by Actors only!";
    }
}

and last but not the least, my
appsettings.json -

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "optimumDB": "Data Source=blablabla;Initial Catalog=ForSideDB;Integrated Security=True;TrustServerCertificate=True"
  },
  "AuthSettings": {
    "JWTSecret": "thisisyoursecrethashingkeyandneedstobekeptsecret",
    "validAudience": "http://localhost:4200",
    "validIssuer": "http://www.forsidesystems.com"
  }
}

I checked my code a few times, especially the configurations for identity authentication and Jwt. I am not sure where the problem is. My knowledge of jwt token based authentication and authorization is rather basic so I am not sure where, which portion and how to debug the code either, in order to locate the anomaly. I have provided most of the relevant code blocks so that this can be reproduced at your end. Please pardon me if the post got too long, I really wanted to post as much code as possible. Please help me identify the problem and get it right.

Looking for some help
Thanks in anticipation,

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,704 questions
ASP.NET
ASP.NET
A set of technologies in the .NET Framework for building web applications and XML web services.
3,547 questions
SQL Server
SQL Server
A family of Microsoft relational database management and analysis systems for e-commerce, line-of-business, and data warehousing solutions.
14,215 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,152 questions
ASP.NET API
ASP.NET API
ASP.NET: A set of technologies in the .NET Framework for building web applications and XML web services.API: A software intermediary that allows two applications to interact with each other.
355 questions
0 comments No comments
{count} votes

Accepted answer
  1. Bruce (SqlWork.com) 68,486 Reputation points
    2024-11-05T18:55:01.2033333+00:00

    I don't see any code to add the roles to the jwt ticket. something like:

    List<Claim> claims = new List<Claim>
    {
       new Claim(JwtRegisteredClaimNames.Aud, authSettings.GetSection("validAudience").Value!),
       new Claim(JwtRegisteredClaimNames.Iss, authSettings.GetSection("validIssuer").Value!),
       new Claim(JwtRegisteredClaimNames.Email, user.Email ?? ""),
       new Claim(JwtRegisteredClaimNames.Name, user.FullName ?? ""),
       new Claim(JwtRegisteredClaimNames.NameId, user.Id ?? "")
    };
    var userRoles = await userManager.GetRolesAsync(user);
    foreach (var userRole in userRoles)
    {
       claims.Add(new Claim(ClaimTypes.Role, userRole));
    }
    
    1 person found this answer helpful.
    0 comments No comments

4 additional answers

Sort by: Most helpful
  1. SurferOnWww 3,536 Reputation points
    2024-11-06T01:12:18.3566667+00:00

    403 Forbidden Error while trying to access authorized routes

    [Authorize(Roles = "Actor")]

    The 403 Forbidden means that user has not been authorized because the "Actor" role claim is not included in the ClaimsIdentity obtained from your JWT. (user has been authenticated, though)

    Decode your JWT at the JWT site and check if your JWT has the "Actor" role claim. Sample of decoded JWT which has "Admin" role is shown below. Your JWT must have "Actor" role.

    enter image description here

    If your JWT does not have the "Actor" role, try adding the claim in your CreateToken method as follows:

    List<Claim> claims = new List<Claim>
    {
        // your existing code to add claims (code omitted)
    
        // Add "Actor" role claim
        new Claim(ClaimTypes.Role, "Actor")
    };
    

    Then, check if the user is authorized as expected.

    1 person found this answer helpful.
    0 comments No comments

  2. SurferOnWww 3,536 Reputation points
    2024-11-06T01:13:15.81+00:00

    this answer has been deleted because of duplication

    0 comments No comments

  3. P. G. Choudhury 61 Reputation points
    2024-11-06T11:14:19.5566667+00:00

    Hi @SurferOnWww and @Bruce (SqlWork.com)

    As per suggestion, I debugged my jWT token on the jwt.io site and found out that the payload data doesn't have any Role in the list of claims.

    I cross checked the code of CreateToken method where I'm generating the token and realized that I haven't added Role in the list of claims. Hence proceeded to use the code you suggested to add Role to the list of claims.

    This code -->

    private async Task<string> CreateToken(AppUser user)
    {
        IConfigurationSection? authSettings = _configuration.GetSection("AuthSettings");
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        byte[] securityKey = Encoding.ASCII.GetBytes(authSettings.GetSection("JWTSecret").Value!);
        List<Claim> claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Aud, authSettings.GetSection("validAudience").Value!),
            new Claim(JwtRegisteredClaimNames.Iss, authSettings.GetSection("validIssuer").Value!),
            new Claim(JwtRegisteredClaimNames.Email, user.Email ?? ""),
            new Claim(JwtRegisteredClaimNames.Name, user.FullName ?? ""),
            new Claim(JwtRegisteredClaimNames.NameId, user.Id ?? "")
        };
        var userRoles = await _userManager.GetRolesAsync(user);
        foreach (var userRole in userRoles)
        {
            claims.Add(new Claim(ClaimTypes.Role, userRole));
        }
        SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.AddDays(1),
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(securityKey),
                SecurityAlgorithms.HmacSha256
            )
        };
        SecurityToken token = tokenHandler.CreateToken(descriptor);
        return tokenHandler.WriteToken(token);
    }
    

    I verified again using the JWT Debugger and found the payload data containing Role. Finally tried out the protected api endpoint again using Swagger, after providing token in the Authorization popup. IT'S WORKING ABSOLUTELY FINE THIS TIME!

    Thanks for helping me resolve this scenario, both of you guided me in the correct direction. My workflow is behaving as expected!

    0 comments No comments

  4. Deleted

    This answer has been deleted due to a violation of our Code of Conduct. The answer was manually reported or identified through automated detection before action was taken. Please refer to our Code of Conduct for more information.


    Comments have been turned off. Learn more

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.