Reputation: 4035
I am trying to access a jwt controller in my dotnet core 3.1 solution.
Here is my jwt controller:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class JwtController : ControllerBase
{
#region Variables
private readonly IUserAuthorisationServices _tokenService;
private readonly IOptions<JwtTokenOptions> jwtOptions;
private readonly ILogger logger;
private readonly JsonSerializerSettings _serializerSettings;
#endregion
public JwtController(IUserAuthorisationServices tokenService,
IOptions<JwtTokenOptions> jwtOptions,
ILoggerFactory loggerFactory)
{
if (loggerFactory is null) throw new ArgumentNullException(nameof(loggerFactory));
_tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
jwtOptions = jwtOptions ?? throw new ArgumentNullException(nameof(jwtOptions));
logger = loggerFactory.CreateLogger<JwtController>();
_serializerSettings = new JsonSerializerSettings {
Formatting = Formatting.Indented
};
//loggingRepository = _errorRepository;
//ThrowIfInvalidOptions(this.jwtOptions);
}
[AllowAnonymous]
[Route("authenticate")]
[HttpPost]
public async Task<IActionResult> Authenticate([FromBody] LoginViewModel model)
{
var userContext = _tokenService.Authenticate(model.Email, model.Password);
if (userContext.Principal == null) {
logger.LogInformation($"Invalid username ({model.Email}) or password ({model.Password})");
return BadRequest(new { message = "Username or password is incorrect" });
}
return Ok(await _tokenService.CreateTokenAsync(userContext).ConfigureAwait(false));
}
[Route("JWTStatus")]
[HttpGet]
public static IActionResult GetJwtStatus()
{
// It made it here so it was authenticated.
return new OkResult();
}
}
I have stepped my way through the process and it gets to this middleware and then cannot find the controller returning immediately and at the same time providing the browser with a 404.
This is what I am using to access this controller action:
login(email: string, password: string) {
this.userLogin.Email = email;
this.userLogin.Password = password;
// Do the fetch!
const t = fetch("/api/authenticate",
{
method: "POST",
body: JSON.stringify(this.userLogin),
headers: new Headers({ "content-type": "application/json" })
})
.then(response => {
I note the fetch is using "api/authenticate" and when I look at the HttpRequest its got the same url yet it wont find it.
Here is the middleware that it gets to before just returning.
namespace JobsLedger.AUTHORISATION.API.SessionMiddleware
{
public class ConfigureSessionMiddleware
{
private readonly RequestDelegate _next;
public ConfigureSessionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext, IUserSession userSession, ISessionServices sessionServices)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (userSession == null)
{
throw new ArgumentNullException(nameof(userSession));
}
if (sessionServices == null)
{
throw new ArgumentNullException(nameof(sessionServices));
}
if (httpContext.User.Identities.Any(id => id.IsAuthenticated))
{
if (httpContext.Session.GetString("connectionString") == null) // Session needs to be set..
{
userSession.UserId = httpContext.User.Claims.FirstOrDefault(x => x.Type == "userId")?.Value;
userSession.ConnectionString = sessionServices.ConnectionStringFromUserId(userSession.UserId);
httpContext.Session.SetString("userId", userSession.UserId);
httpContext.Session.SetString("connectionString", userSession.ConnectionString);
}
else // Session set so all we need to is to build userSession for data access..
{
userSession.UserId = httpContext.Session.GetString("userId");
userSession.ConnectionString = httpContext.Session.GetString("connectionString");
}
}
// Call the next delegate/middleware in the pipeline
await _next.Invoke(httpContext).ConfigureAwait(false);
}
}
}
Its suppose to jump straight past this as there are no user identities as this hasnt been set yet. So really it goes straight to:
await _next.Invoke(httpContext).ConfigureAwait(false);
Then onto the controller action.. but it just goes straight out with a 404.
In the output window I am getting:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/2.0 POST https://localhost:44301/api/authenticate text/plain;charset=UTF-8 76
Microsoft.AspNetCore.Cors.Infrastructure.CorsService: Information: CORS policy execution successful.
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 17.1363ms 404
You can see the 404.
How is it cant find the action?
Is it possibly due to it being https and http 2.0?
For completeness here is the httpContext at the point of that middleware:
UPDATE...
I do not use MVC in the startup either just endpoints...
Maybe its the pipeline in Startup.cs.. here is my startup.cs
[assembly: NeutralResourcesLanguage("en")]
namespace JobsLedger.API {
public class Startup {
//private const string SecretKey = "needtogetthisfromenvironment";
//private readonly SymmetricSecurityKey
// _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _configuration; // { get; }
public Startup(IWebHostEnvironment env, IConfiguration configuration) {
_env = env;
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
// Add framework services.
services.AddCors();
services.AddControllers();
services.AddOptions();
services.AddDbContext<CATALOGContext>(options => options.UseSqlServer(_configuration.GetConnectionString("CatalogConnection"), b => b.MigrationsAssembly("JobsLedger.CATALOG")));
services.AddDbContext<DATAContext>(options => options.UseSqlServer(_configuration.GetConnectionString("TenantDbConnection"), a => a.MigrationsAssembly("JobsLedger.DATA")));
// Make authentication compulsory across the board (i.e. shut
// down EVERYTHING unless explicitly opened up).
// Use policy auth.
services.AddAuthorization(options => {
options.AddPolicy("TenantAdmin", policy => policy.RequireClaim(ClaimTypes.Role, "TenantAdmin"));
options.AddPolicy("Admin", policy => policy.RequireClaim(ClaimTypes.Role, "Admin"));
options.AddPolicy("Employee", policy => policy.RequireClaim(ClaimTypes.Role, "Employee"));
});
//services.ConfigureApplicationInjection();
// Get jwt options from app settings
var tokenOptions = _configuration.GetSection("Authentication").Get<JwtTokenOptions>();
services.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(tokenOptions.SigningKey)),
ClockSkew = TimeSpan.Zero
};
});
services
.AddDistributedMemoryCache()
.AddSession()
// Repositories - DATA
.AddScoped<IClientDATARepository, ClientDATARepository>()
.AddScoped<ILoggingDATARepository, LoggingDATARepository>()
.AddScoped<IJobDATARepository, JobDATARepository>()
.AddScoped<IBrandDATARepository, BrandDATARepository>()
.AddScoped<ITypeDATARepository, TypeDATARepository>()
.AddScoped<IStateDATARepository, StateDATARepository>()
.AddScoped<IStatusDATARepository, StatusDATARepository>()
.AddScoped<ISuburbDATARepository, SuburbDATARepository>()
.AddScoped<ICounterDATARepository, CounterDATARepository>()
// Repositories - CATALOG
.AddScoped<ITenantCATALOGRepository, TenantCATALOGRepository>()
.AddScoped<IUserCATALOGRepository, UserCATALOGRepository>()
.AddScoped<IRoleCATALOGRepository, RoleCATALOGRepository>()
.AddScoped<ICounterCATALOGRepository, CounterCATALOGRepository>()
.AddScoped<ISuburbCATALOGRepository, SuburbCATALOGRepository>()
.AddScoped<IStateCATALOGRepository, StateCATALOGRepository>()
// Business services
// Services - API
.AddScoped<IClientServices, ClientServices>()
.AddScoped<IJobServices, JobServices>()
//Services - Catalog
.AddScoped<ITenantServices, TenantServices>()
.AddScoped<IUserServices, UserServices>()
//.AddScoped<IUserValidateService, UserValidateService>()
// Services - Shared
.AddScoped<IAddressDropdownServices, AddressDropdownServices>()
//Services - Auth
.AddScoped<ICryptoService, CryptoService>()
.AddScoped<IUserAuthorisationServices, UserAuthorisationServices>()
.AddScoped<IUserSession, UserSession>()
.AddScoped<ISessionServices, SessionServices>()
// CATALOG services - Initialisations.
.AddScoped<ICATALOGCounterInitialiser, CATALOGCounterInitialiser>()
.AddScoped<ICATALOGStateInitialiser, CATALOGStateInitialiser>()
.AddScoped<ICATALOGSuburbInitialiser, CATALOGSuburbInitialiser>()
.AddScoped<IRoleInitialiser, CATALOGRoleInitialiser>()
.AddScoped<ICATALOGTenantAndUserInitialisations, CATALOGTenantAndUserInitialiser>()
// DATA Services - Initialisations.
.AddScoped<IBrandInitialiser, BrandInitialiser>()
.AddScoped<IDATACounterInitialiser, DATACounterInitialiser>()
.AddScoped<IDATAStateInitialiser, DATAStateInitialiser>()
.AddScoped<IDATASuburbInitialiser, DATASuburbInitialiser>()
.AddScoped<IStatusInitialiser, StatusInitialiser>()
.AddScoped<ITypeInitialiser, TypeInitialiser>()
.AddScoped<IVisitTypeInitialiser, VisitTypeInitialiser>()
// TESTDATA Services - Initialisations.
.AddScoped<ITESTDATAInitialisations, TESTDATAInitialisations>()
.AddScoped<ITESTDATATenantAndUserInitialisations, TESTDATATenantAndUserInitialisations>()
.AddTransient<IDATAContextFactory, DATAContextFactory>()
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); // For getting the user.
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
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.UseExceptionHandler(options => {
options.Run(async context => {
var ex = context.Features.Get<IExceptionHandlerPathFeature>();
if (ex?.Error != null) {
Debugger.Break();
}
});
});
app.UseRouting();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseFileServer();
app.UseStaticFiles();
app.UseSession();
app.UseAuthorization();
app.UseConfigureSession();
app.EnsureCATALOGMigrationAndInitialise();
app.EnsureDATAMigrationsAndInitialise();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
}
Upvotes: 2
Views: 8035
Reputation: 32069
Okay! got it. According to your current route configuration, your routing endpoints are:
/authenticate // <-- not api/authenticate
/JWTStatus // <-- not api/JWTStatus
Because you are overriding the controller level route in action.
If you want api/authenticate
and api/JWTStatus
as your routing endpoints, then your action level route should be as follows:
[Authorize]
[ApiController]
//Remove the controller level route from here
public class JwtController : ControllerBase
{
[AllowAnonymous]
[Route("api/authenticate")] // <-- Here it is
[HttpPost]
public async Task<IActionResult> Authenticate([FromBody] LoginViewModel model)
{
.........
}
[Route("api/JWTStatus")] // <-- Here it is
[HttpGet]
public IActionResult GetJwtStatus()
{
.......
}
}
But my suggestion is to follow the following convention:
[Authorize]
[ApiController]
[Route("api/[controller]/[action]")] // <-- Here it is
public class JwtController : ControllerBase
{
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Authenticate([FromBody] LoginViewModel model)
{
.......
}
[HttpGet]
public IActionResult GetJwtStatus()
{
......
}
}
Now no action level route is required anymore and your endpoints will be:
api/Jwt/Authenticate
api/Jwt/GetJwtStatus
Upvotes: 5
Reputation: 2340
Your controller level route attribute is creating problem.
Both route attributes, at controller and action level, need a change.
First, remove your controller's route attribute
Then, modify action level route attributes as below
[Route("api/authenticate")]
[Route("api/JWTStatus")]
Upvotes: 2