I spent a good few hours rummaging around on the internet, tried various solutions, and unfortunately to no avail. The problem is that he wants to download data for the currently logged in user (his object or at least the identifier for retrieving the object). Unfortunately, with every possible solution I get null, I have no ideas what to do, and I would like to fix this problem. Any ideas? .NET 6.0 version, see title.
Hope we can solve the problem together. :)
Code:
Program.cs
using KursAspNetBackend.Database;
using KursAspNetBackend.Domain.Entities;
using KursAspNetBackend.Database.Repositories;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using KursAspNetBackend.Domain.Interfaces.Identity;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.Configure<IdentityOptions>(options => options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
//builder.Services.AddHttpContextAccessor();
///builder.Services.AddTransient<IPrincipal>(provider => provider.GetService<IHttpContextAccessor>().HttpContext.User);
//builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
}).AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Adding Transients
builder.Services.AddTransient<IMessagesRepository, MessagesRepository>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
}
app.UseAuthentication();
//app.UseIdentityServer();
app.UseAuthorization();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true)
.AllowCredentials()); // allow credentials
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html"); ;
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.EnsureCreated();
}
//app.Services.GetRequiredService<ApplicationDbContext>().Database.EnsureCreated();
app.Run();
AccountController.cs:
using KursAspNetBackend.Domain.Entities;
using KursAspNetBackend.Domain.Dtos;
using KursAspNetBackend.Domain.Entities;
using KursAspNetBackend.Domain.Dtos;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace KursAspNetBackend.Controllers
{
[Route("account/")]
public class AccountController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IHttpContextAccessor _httpContextAccessor;
public AccountController(UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_signInManager = signInManager;
_httpContextAccessor = httpContextAccessor;
}
[HttpGet]
[Route("getCurrentUser")]
public async Task<IActionResult> GetCurrentUser()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User);
if(user == null)
{
return Unauthorized();
}
return Ok(user);
}
[HttpPost]
[Route("register")]
public async Task<IActionResult> Register([FromBody] UserRegisterDto userRegisterDto)
{
var newUser = new ApplicationUser
{
Email = userRegisterDto.Email,
UserName = userRegisterDto.Email,
FirstName = userRegisterDto.FirstName,
LastName = userRegisterDto.LastName,
Address = "",
};
var result = await _userManager.CreateAsync(newUser, userRegisterDto.Password);
if (result.Succeeded)
{
var token = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
await _userManager.ConfirmEmailAsync(newUser, token);
return Ok();
}
else
{
foreach (IdentityError error in result.Errors)
Console.WriteLine($"Oops! {error.Description} ({error.Code}");
}
return NotFound();
}
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLoginDto)
{
var foundUser = await _userManager.FindByEmailAsync(userLoginDto.Email);
if(foundUser == null)
{
return NotFound();
}
var result = await _signInManager.PasswordSignInAsync(foundUser, userLoginDto.Password, true, false);
if (result.Succeeded)
{
return Ok();
}
return NotFound();
}
}
}
lounchSettings.json
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:12761",
"sslPort": 0
}
},
"profiles": {
"KursAspNetBackend": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5054",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
}
}
}
Related
I am trying to setup a custom authorization handler in my project closely following the Resource-based authorization in ASP.NET Core Microsoft documentation. However, calling the API endpoint always returns a 403 response and doesn't even trigger a breakpoint in my handler.
Feels like I quadruple checked every single thing yet it still doesn't work. Am I blind or is there something wrong with my code?
Here's my custom handler:
public class TripAuthorizationHandler : AuthorizationHandler<SameUserRequirement, Trip>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SameUserRequirement requirement, Trip resource)
{
if(context.User.FindFirst("userId").Value == resource.ApplicationUserId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class SameUserRequirement : IAuthorizationRequirement{}
Program.cs file:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SameUser", policy => policy.Requirements.Add(new SameUserRequirement()));
});
builder.Services.AddTransient<IAuthorizationHandler, TripAuthorizationHandler>();
builder.Services.AddTransient<IAuthenticationService, AuthenticationService>();
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
And here's the controller:
[Authorize(Policy = "SameUser")]
[HttpPut("trips/{tripId}")]
public async Task<IActionResult> UpdateTrip([FromBody] TripPostDto request, int tripId)
{
var trip = await _tripService.GetByIdAsync(tripId);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, trip, "SameUser");
if (authorizationResult.Succeeded)
{
await _tripService.UpdateAsync(tripId, request);
return NoContent();
}
else if (User.Identity.IsAuthenticated)
{
return Forbid();
}
else
{
return Challenge();
}
}
Edit:
Here's my implementation of AuthenticationService
public class AuthenticationService : IAuthenticationService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ITokenService _tokenService;
private readonly IMapper _mapper;
public AuthenticationService(UserManager<ApplicationUser> userManager, IMapper mapper, ITokenService tokenService)
{
_userManager = userManager;
_mapper = mapper;
_tokenService = tokenService;
}
public async Task<SuccessfulLoginDto> LoginAsync(LoginUserDto loginUserDto)
{
var user = await _userManager.FindByEmailAsync(loginUserDto.Email);
if(user == null)
{
throw new ValidationException("Invalid login data!");
}
var passwordValid = await _userManager.CheckPasswordAsync(user, loginUserDto.Password);
if (!passwordValid)
{
throw new ValidationException("Invalid login data!");
}
var accessToken = await _tokenService.GetTokenAsync(user);
return new SuccessfulLoginDto() { Token = accessToken.AccessToken };
}
public async Task RegisterAsync(RegisterUserDto registerUserDto)
{
var existingUser = await _userManager.FindByEmailAsync(registerUserDto.Email);
if (existingUser != null)
{
throw new UserAlreadyExistsException("User with this email already exists!");
}
var newUser = _mapper.Map<ApplicationUser>(registerUserDto);
var result = await _userManager.CreateAsync(newUser, registerUserDto.Password);
if (!result.Succeeded)
{
throw new ValidationException(string.Join(" ", result.Errors.Select(e => e.Description)));
}
await _userManager.AddToRoleAsync(newUser, Authorization.Roles.User.ToString());
}
}
Have you tried ordering the service injection before?
Order matters in Startup
builder.Services.AddTransient<IAuthorizationHandler, TripAuthorizationHandler>();
builder.Services.AddTransient<IAuthenticationService, AuthenticationService>();
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SameUser", policy => policy.Requirements.Add(new SameUserRequirement()));
});
I have a simple .NET6 OData API with the default configuration with Batch enabled.
The API is configured with IdentityServer from the default VS template.
Program.cs
var builder = WebApplication.CreateBuilder(args);
if (builder.Configuration.GetSection("ConnectionStrings:Active").Value == "Postgres")
{
var connectionString = builder.Configuration.GetConnectionString("Postgres");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
}
else
{
var connectionString = builder.Configuration.GetConnectionString("SQLServer");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
}
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/login";
options.UserInteraction.LogoutUrl = "/logout";
})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
builder.Services.AddLocalization();
builder.Services.AddControllersWithViews(options =>
{
options.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
})
.AddOData(opt =>
{
var batchHandler = new DefaultODataBatchHandler();
batchHandler.MessageQuotas.MaxNestingDepth = 2;
batchHandler.MessageQuotas.MaxReceivedMessageSize = 100;
batchHandler.MessageQuotas.MaxOperationsPerChangeset = 10;
opt.AddRouteComponents("oapi",
new OdataModelBuilder().GetEDM(),
services => services.AddSingleton<ISearchBinder, ODataSearch>());
opt.AddRouteComponents("oapi_b",
new OdataModelBuilder().GetEDM(),
batchHandler);
opt.EnableQueryFeatures();
})
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResources));
})
.AddRazorRuntimeCompilation();
builder.Services.AddRazorPages();
builder.Services.AddAutoMapper(System.Reflection.Assembly.GetExecutingAssembly());
builder.Services.AddHttpContextAccessor();
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<DummySeedService>();
}
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
app.UseODataRouteDebug();
using (var scope = app.Services.CreateScope())
{
var seedService = scope.ServiceProvider.GetRequiredService<DummySeedService>();
await seedService.Seed();
}
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseODataBatching();
app.UseRouting();
app.UseMiddleware<ODataTestMiddleware>();
var supportedCultures = new[] { "en-US", "ar-SY" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
The Problem
After making a $batch request, when execution falls out of the UseODataBatching middleware, the HttpContext property in the IHttpContextAccessor becomes null, and this triggers a NullReferenceException in the IdentityServer middleware thus returning 500 for all requests
Requests (from Postman):
{
"requests": [
{
"id": "{{$guid}}",
"url": "Patients(1)",
"method": "GET",
"headers": {
"content-type": "application/json"
}
},
{
"id": "{{$guid}}",
"url": "Patients(2)",
"method": "GET",
"headers": {
"content-type": "application/json"
}
}
]
}
Responses:
{
"responses": [
{
"id": "8a4dac81-2662-472b-bef9-273401a53cfb",
"status": 500,
"headers": {}
},
{
"id": "933c6bbe-db67-4526-8199-2eedc176dc7b",
"status": 500,
"headers": {}
}
]
}
When removing the IdentityServer middleware the batch request gets through and returns 200 for both requests with no problems.
And as a test i wrote a test middleware ODataTestMiddleware
public class ODataTestMiddleware
{
private readonly RequestDelegate requestDelegate;
public ODataTestMiddleware(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext, IHttpContextAccessor contextAccessor)
{
await requestDelegate(httpContext);
}
}
And IHttpContextAccessor.HttpContext is also null in here.
I saw this issue on the OData repo IHttpContextAccessor.HttpContext returns null when executing within an odata batch call, but i am using the ASP.NET Core version so i don't know the difference between the two in terms of implementation.
Is there a workaround for this that i can try?
Thank you for your time.
I ended up adding a middleware that repopulates the IHttpContextAccessor manually.
public class ODataHttpContextFixer
{
private readonly RequestDelegate requestDelegate;
public ODataHttpContextFixer(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext, IHttpContextAccessor contextAccessor)
{
contextAccessor.HttpContext ??= httpContext;
await requestDelegate(httpContext);
}
}
The middleware should be placed after the ODataBatching middleware.
I am trying out Web API's in .net core. I have added an API controller to my existing .net core MVC web application.
I am using JWT Tokens for authorization. I am doing this through the startup class.
The API works just fine.
But as expected the authorization is being applied on the entire MVC application.
Is there a way I can enable authentication through bearer tokens only for the API controllers and not the web application controllers?
Startup.cs:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Project.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace Project
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public bool ValidateAudience { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser,IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/Identity/Account/Login";
options.LogoutPath = "/Identity/Account/Logout";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});
services.AddCors();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["Jwt:Site"],
ValidIssuer = Configuration["Jwt:Site"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SigningKey"]))
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{Controller=Startup}/{action=Login}/{id?}");
});
}
}
}
appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": " Data Source=Server; Database = db;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Jwt": {
"Site": "www.signinkey.com",
"SigningKey": "ConstantSigningKey",
"ExpiryInMinutes": "30"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
API controller:
namespace Project.Controllers
{
[Route("api/[controller]/[action]")]
public class AuthController : BaseController
{
protected IConfiguration _configuration;
public AuthController(UserManager<IdentityUser> userManager, IConfiguration configuration)
{
_userManager = userManager;
_configuration = configuration;
}
public string GenerateToken(int size = 32)
{
var randomNumber = new byte[size];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
[HttpGet("")]
[Route("modelList")]
[Authorize]
public IEnumerable<ModelList> SupervisorList(string username)
{
return db.modelList.Select(x => x).ToList();
}
[Route("register")]
[HttpPost]
public async Task<ActionResult> Register([FromBody] InputModel reg)
{
var user = new IdentityUser { UserName = reg.Email, Email = reg.Email, SecurityStamp = Guid.NewGuid().ToString() };
var result = await _userManager.CreateAsync(user, reg.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Admin");
}
return Ok(new { Username = user.UserName });
}
[Route("login")]
[HttpPost]
public async Task<ActionResult> Login([FromBody] InputModel login)
{
var user = await _userManager.FindByNameAsync(login.Email);
if (user != null && await _userManager.CheckPasswordAsync(user, login.Password))
{
var claim = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName)
};
var signinKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SigningKey"]));
int expiryInMinutes = Convert.ToInt32(_configuration["Jwt:ExpiryInMinutes"]);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Site"],
audience: _configuration["Jwt:Site"],
expires: DateTime.UtcNow.AddMinutes(expiryInMinutes),
signingCredentials: new SigningCredentials(signinKey, SecurityAlgorithms.HmacSha256)
);
return Ok(
new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
}
}
As the JWT Token authentication is enabled in the startup class; I am getting an 401 unauthorized code when I try to access the non api controller actions as well.
What I am trying is:
Add an API controller to an existing .net core mvc web application.
Use JWT token authentication only for the API methods and not the web application methods.
Is the above possible? Is it a good practice? How can it be achieved?
Need Direction. Thank you:)
Detailed Articles
Using Multiple Authentication/Authorization Providers in ASP.NET Core
Yes you can here are few steps
Setup multiple authentication schemes in Startup.cs
Use scheme as per controller
Specify the Policy for web application and used that in Web app controllers
[Authorize(Policy = "WebApp")]
In Web API controllers just use the JWT authentication scheme
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Startup code
.AddCookie(options =>
{
// You cookie auth setup
})
.AddJwtBearer(options =>
{
// Your JWt setup
})
policies Setup
services.AddAuthorization(options =>
{
options.AddPolicy("WebApp",
policy => policy.Requirements.Add(new WebAppRequirement()));
});
You can write your own authorization attribute in .NET. Like [CustomAuthorization] instead of built-in attributer. For learning phase of course.
There was no problem until I added a new controller to my ASP.NET Core (v2.1) Web API. Now, all my controllers are giving 404 error. The last added controller is UsersController. I'm not sure if I've changed something else.
UsersController.cs
using System.Threading.Tasks;
using DatingApp.API.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DatingApp.API.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IDatingRepository _repo;
public UsersController(IDatingRepository repo)
{
_repo = repo;
}
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _repo.GetUsers();
return Ok(users);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var user = await _repo.GetUser(id);
return Ok(user);
}
}
}
launchSettings.json
{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"DatingApp.API": {
"commandName": "Project",
"launchBrowser": false,
"launchUrl": "api/values",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seed seeder)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(builder => {
builder.Run(async context => {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if(error != null)
{
context.Response.AddApplicationError(error.Error.Message);
await context.Response.WriteAsync(error.Error.Message);
}
});
// app.UseHsts();
});
// app.UseHttpsRedirection();
// seeder.SeedUsers();
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseAuthentication();
app.UseMvc();
}
}
I've tried to restart Kestrel web server until now. Did not work for me.
I'm working on building an API for handling identity stuff in .NET Core, but every time I try and make a call I get a 404.
There didn't seem to be anything clear when I looked around for an answer, since the code posted seems quite minimal. Here's all the stuff I think is pertinent.
The Controller:
using Common.Extensions;
using Identity.Database.Contexts.Models;
using Identity.WebApi.Models;
using Identity.WebApi.Models.Tokens;
using Identity.WebApi.Services.Access;
using Identity.WebApi.Services.Accounts;
using Identity.WebApi.Services.Tokens;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Controller = Microsoft.AspNetCore.Mvc.Controller;
using Get = Microsoft.AspNetCore.Mvc.HttpGetAttribute;
using Post = Microsoft.AspNetCore.Mvc.HttpPostAttribute;
using Route = Microsoft.AspNetCore.Mvc.RouteAttribute;
namespace Identity.WebApi.Controllers
{
[Route("api/[controller]")]
public class IdentityController : Controller
{
private readonly IApplicationUserService _userService;
private readonly IAccessService _accessService;
private readonly ITokenService _tokenService;
private readonly SignInManager<ApplicationUser> _signInManager;
public IdentityController(IApplicationUserService userService, IAccessService accessService, ITokenService tokenService, SignInManager<ApplicationUser> signInManager)
{
_userService = userService;
_accessService = accessService;
_tokenService = tokenService;
_signInManager = signInManager;
}
[Get]
[AllowAnonymous]
public string Index()
{
return new Dictionary<string,string>
{
{ "status", "live" }
}.Serialize();
}
[Post]
[Route("create")]
[AllowAnonymous]
public Task<ISet<IdentityResult>> Create(string user)
{
var decodedUser = DecodeUser(user);
var applicationUser = new ApplicationUser(new User
{
Id = Guid.NewGuid(),
Name = decodedUser.Username,
LastActive = DateTime.UtcNow
});
return _userService.Add(applicationUser, decodedUser.Password);
}
private (string Username, string Password) DecodeUser(string encodedUser)
{
var decodedUser = encodedUser.DecodeFrom64().Split(':');
return (Username: decodedUser[0], Password: decodedUser[1]);
}
private async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
=> await _signInManager.UserManager.CheckPasswordAsync(user, password);
}
}
The Startup:
using Identity.Database.Contexts;
using Identity.Database.Contexts.Access;
using Identity.Database.Contexts.Extensions;
using Identity.Database.Contexts.Models;
using Identity.WebApi.Models;
using Identity.WebApi.Services.Access;
using Identity.WebApi.Services.Accounts;
using Identity.WebApi.Services.Certs;
using Identity.WebApi.Services.Tokens;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.CompilerServices;
namespace Identity.WebApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new CertService(Configuration) as ICertService)
.AddTransient<IApplicationUserService, ApplicationUserService>()
.AddTransient<IApplicationRoleService, ApplicationRoleService>()
.AddTransient<IAccessService, AccessService>()
.AddTransient<ICertService, CertService>()
.AddTransient<ITokenService, TokenService>()
.AddTransient<ICrudDao<AppDbContext, Role>, RoleDao>()
.AddIdentities<ApplicationUser, ApplicationRole>()
.AddScoped<UserManager<ApplicationUser>, UserManager<ApplicationUser>>()
.AddScoped<SignInManager<ApplicationUser>, SignInManager<ApplicationUser>>()
.AddScoped<RoleManager<ApplicationRole>, RoleManager<ApplicationRole>>()
.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (c, n) =>
{
await n();
if (c.Response.StatusCode == 404)
{
c.Request.Path = "/identity";
await n();
}
});
app.UseStaticFiles();
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(r => { r.MapRoute(name: "default", template: "{controller=identity}/{action=Index}"); });
}
}
}
The launch settings:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55048/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/identity/index",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"WebApplication1": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/identity/index",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:55048/"
}
}
}
At the top of your controller you have:
[Route("api/[controller]")]
public class IdentityController : Controller
Which means that if your route starts with api/ only then it will match the controller. Also, your Index action doesn't have any extra routing attributes on it, so its looking for api/identity only. However, your launch settings don't match that part, and since you don't have any other routes matching it, you get a 404.
The default route in app.UseMvc won't work for this reason.
Simple fix: change launchUrl to just api/identity in your launch settings... and then follow #Nkosi's answer
If using attribute routing then there is no api/identity/index as [HttpGet] or Get in your example with a route prefix, is the same as
[Get] //<-- Matches GET api/identity
[AllowAnonymous]
public IActionResult Index() {
var result = new Dictionary<string,string>
{
{ "status", "live" }
}.Serialize();
return Ok(result);
}
And since this appears to be a Web API that is not expected to return a View then the Http{Verb} attribute with a route template would be the option to use for routing
When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.
[Post("create")] //<-- Matches POST api/identity/create
[AllowAnonymous]
public async Task<IActionResult> Create(string user) {
var decodedUser = DecodeUser(user);
var applicationUser = new ApplicationUser(new User
{
Id = Guid.NewGuid(),
Name = decodedUser.Username,
LastActive = DateTime.UtcNow
});
ISet<IdentityResult> result = await _userService.Add(applicationUser, decodedUser.Password);
return Ok(result);
}
Reference Routing to Controller Actions