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,