I have developed a Core 2 MVC app with Individual User Accounts using standard Identity. The MVC app is all working fine.
I am trying to add a public API and authorize requests using JWT.
However, something is going wrong when it is trying to authorize the user. When I submit a request to the controller, it redirects me to my Login page, so the response body is HTML.
I need to to authorize and return data (or not if the token is invalid).
What have I done wrong? The token generation is all OK.
Token Controller
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using TechsportiseOnline.Models.AccountViewModels;
using Microsoft.AspNetCore.Identity;
using TechsportiseOnline.Models;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using TechsportiseOnline.Helpers;
namespace TechsportiseOnline.Controllers
{
[Produces("application/json")]
[Route("api/Token")]
public class TokenController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IConfiguration _configuration;
private readonly IOptions<JWTSettings> _jwtConfig;
public TokenController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IConfiguration configuration,
IOptions<JWTSettings> jwtConfig)
{
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
_jwtConfig = jwtConfig;
}
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> GenerateToken([FromBody] LoginViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
{
var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false);
if (result.Succeeded)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Value.SecretKey.ToString()));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_jwtConfig.Value.Issuer.ToString(),
_jwtConfig.Value.Audience.ToString(),
claims,
expires: DateTime.Now.AddDays(30),
signingCredentials: creds);
return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
}
}
return BadRequest("Could not create token");
}
}
}
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TechsportiseOnline.Data;
using TechsportiseOnline.Models;
using TechsportiseOnline.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using TechsportiseOnline.Authorization;
using TechsportiseOnline.Helpers;
using Swashbuckle.AspNetCore.Swagger;
using System.IO;
using Microsoft.Extensions.PlatformAbstractions;
using static TechsportiseOnline.Helpers.Swagger;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace TechsportiseOnline
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("TechsportiseDB")));
//options.UseInMemoryDatabase("Teschsportise"));
services.Configure<JWTSettings>(Configuration);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.IncludeErrorDetails = true;
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
var secretKey = Configuration.GetSection("JWTSettings.SecretKey").Value;
var issuer = Configuration.GetSection("JWTSettings.Issuer").Value;
var audience = Configuration.GetSection("JWTSettings.Audience").Value;
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("JWTSettings.Issuer").Value,
ValidateAudience = true,
ValidAudience = Configuration.GetSection("JWTSettings.Audience").Value,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWTSettings:SecretKey"]))
};
});
services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings"));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 2;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.Configure<AuthMessageSenderOptions>(Configuration);
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromDays(150);
options.LoginPath = "/Account/Login"; // If the LoginPath is not set here, ASP.NET Core will default to /Account/Login
options.LogoutPath = "/Account/Logout"; // If the LogoutPath is not set here, ASP.NET Core will default to /Account/Logout
options.AccessDeniedPath = "/Account/AccessDenied"; // If the AccessDeniedPath is not set here, ASP.NET Core will default to /Account/AccessDenied
options.SlidingExpiration = true;
});
// Add application services.
services.AddTransient<IEmailSender, Email>();
//services.AddTransient<ICreateContact>();
//services.AddTransient<IUpdateContact>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "Techsportise API", Version = "v1" });
c.OperationFilter<AddRequiredHeaderParameter>();
var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "Techsportise.xml");
c.IncludeXmlComments(filePath);
});
services.AddMvc();
var skipSSL = Configuration.GetValue<bool>("LocalTest:skipSSL");
// requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
// Set LocalTest:skipSSL to true to skip SSL requrement in
// debug mode. This is useful when not using Visual Studio.
if (!skipSSL)
{
options.Filters.Add(new RequireHttpsAttribute());
}
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services.AddScoped<IAuthorizationHandler,
OwnerRaceAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
AdminRaceAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler,
OwnerRaceEntriesAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
AdminRaceEntriesAuthorizationHandler>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Techsportise API V1");
});
}
}
}
RacesController
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TechsportiseOnline.Data;
using TechsportiseOnline.Helpers;
using TechsportiseOnline.Models;
namespace TechsportiseOnline.Controllers
{
/// <summary>
/// This class is used as an API for Races
/// </summary>
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
public class RaceController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IAuthorizationService _authorizationService;
private readonly UserManager<ApplicationUser> _userManager;
public RaceController(ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<ApplicationUser> userManager)
{
_context = context;
_userManager = userManager;
_authorizationService = authorizationService;
}
/// <summary>
/// Get all Races
/// </summary>
/// <remarks>
/// Gets all Races which have been created by the user
/// </remarks>
/// <returns>All created Races</returns>
[HttpGet]
public IEnumerable<Race> GetAll()
{
//Get only records where the OwnerId is not the logged in User.
return _context.Races.Where(p => p.OwnerID == _userManager.GetUserId(User)).ToList();
}
/// <summary>
/// Get a single Race
/// </summary>
/// <remarks>
/// Gets the details from a single Race from it's ID
/// </remarks>
/// <param name="id">Race ID</param>
/// <returns>Single Race</returns>
[HttpGet("{id}", Name = "GetRace")]
public IActionResult GetById(long id)
{
//Only return the data when it is owned by the same Id
var item = _context.Races.Where(p => p.OwnerID == _userManager.GetUserId(User)).FirstOrDefault(t => t.ID == id);
if (item == null)
{
return NotFound();
}
return new ObjectResult(item);
}
/// <summary>
/// Get all entries for a Race
/// </summary>
/// <remarks>
/// Gets the all the entries from the race ID
/// </remarks>
/// <param name="id">Race ID</param>
/// <returns>All Entries from the given Race ID</returns>
[HttpGet("{id}/entries", Name = "GetEntriesByRaceID")]
public IEnumerable<RaceEntry> GetAllEntries(long id)
{
//Only return the data when it is owned by the same Id
//Get only records where the OwnerId is not the logged in User.
return _context.RaceEntries.Where(p => p.OwnerID == _userManager.GetUserId(User))
.Where(p => p.RaceID == id)
.ToList();
}
///// <summary>
///// Get all timings for a Race
///// </summary>
///// <remarks>
///// Gets the all the timings from the race ID
///// </remarks>
///// <param name="id">Race ID</param>
///// <returns>All timings from the given Race ID</returns>
//[HttpGet("{id}/timings", Name = "GetTimingsByRaceID")]
//public IEnumerable<Timing> GetAllTimings(long id)
//{
// //Only return the data when it is owned by the same Id
// //Get only records where the OwnerId is not the logged in User.
// return _context.Timings.Where(p => p.OwnerId == User.GetUserId())
// .Where(p => p.RaceId == id)
// .ToList();
//}
///// <summary>
///// Get the results for a Race
///// </summary>
///// <remarks>
///// Gets the all the results from the race ID
///// </remarks>
///// <param name="id">Race ID</param>
///// <returns>All results from the given Race ID</returns>
//[HttpGet("{id}/results", Name = "GetResultsByRaceID")]
//public IEnumerable<Results> GetAllResults(long id)
//{
// List<Results> raceresults = new List<Results>();
// var raceid = id;
// foreach (var raceentry in _context.RaceEntries.Where(p => p.OwnerId == User.GetUserId())
// .Where(p => p.RaceID == id))
// {
// var raceresult = new Results();
// var racedetails = _context.Races.Where(t => t.OwnerId == User.GetUserId())
// .FirstOrDefault(t => t.Id == raceid);
// var timingdetails = _context.Timings.Where(t => t.OwnerId == User.GetUserId())
// .FirstOrDefault(t => t.BibNumber == raceentry.BibNumber);
// var race = _context.Races.Where(t => t.OwnerId == User.GetUserId())
// .FirstOrDefault(t => t.Id == id);
// raceresult.AthleteUserID = raceentry.AthleteUserId;
// raceresult.Category = "Category";
// raceresult.CategoryPosition = 1;
// raceresult.ChipTime = DateTime.Now; //timingdetails.EndTime - timingdetails.StartTime;
// raceresult.Club = raceentry.Club;
// raceresult.ClubPosition = 1;
// raceresult.EntryId = raceentry.Id;
// raceresult.FirstName = raceentry.FirstName;
// raceresult.Gender = raceentry.Gender;
// raceresult.GenderPosition = 1;
// raceresult.GunTime = DateTime.Now; //race.RaceStartTime - timingdetails.EndTime;
// raceresult.LastName = raceentry.LastName;
// raceresult.OverallPosition = 0;
// raceresult.RaceDate = race.RaceDate;
// raceresult.RaceID = raceid;
// raceresult.RaceName = race.Name;
// raceresult.ResultId = 1;
// raceresult.Team = raceentry.Team;
// raceresult.TeamPosition = 1;
// raceresults.Add(raceresult);
// //build result object
// }
// //Only return the data when it is owned by the same Id
// //Get only records where the OwnerId is not the logged in User.
// return raceresults.ToList();
//}
///// <summary>
///// Publish the results of a Race
///// </summary>
///// <remarks>
///// Publishes the results as Provisional or Final. Final will submit them to RunBritain/PO10
///// </remarks>
///// <returns>The JSON for the created Race</returns>
//[HttpPost("{id}/publish", Name = "PublishResults")]
//public IActionResult Publish([FromBody] Race item)
//{
// if (item == null)
// {
// return BadRequest();
// }
// _context.Races.Add(item);
// //Set Owner ID
// item.OwnerId = User.GetUserId();
// _context.SaveChanges();
// return CreatedAtRoute("GetRace", new { id = item.Id }, item);
//}
/// <summary>
/// Creates a Race
/// </summary>
/// <remarks>
/// Creates a Race which can have entrants and timings assigned to it.
/// </remarks>
[HttpPost]
public IActionResult Create([FromBody] RacePost item)
{
if (item == null)
{
return BadRequest();
}
if (item.Name == null)
{
return BadRequest("The Race must have a Name");
}
var raceitem = new Race
{
CurrentEntries = item.CurrentEntries,
Description = item.Description,
MaxEntries = item.MaxEntries,
Name = item.Name,
ContactName = item.ContactName,
ContactEmail = item.ContactEmail,
ContactNumber = item.ContactNumber,
OwnerID = _userManager.GetUserId(User),
RaceDate = item.RaceDate,
RaceStartTime = item.RaceStartTime,
IsCompleted = item.IsCompleted,
IsPublished = item.IsPublished,
IsOpenForEntries = item.IsOpenForEntries,
LastUpdated = DateTime.Now
};
_context.Races.Add(raceitem);
_context.SaveChanges();
return CreatedAtRoute("GetRace", new { id = raceitem.ID }, raceitem);
}
/// <summary>
/// Update a Race
/// </summary>
/// <remarks>
/// Update's a Race's details
/// </remarks>
/// <param name="id">Race ID</param>
/// <returns>The JSON for the updated Race</returns>
[HttpPut("{id}")]
public IActionResult Update(long id, [FromBody] Race item)
{
if (item == null)
{
return BadRequest();
}
if (item.Name == null)
{
return BadRequest("The Race must have a Name");
}
var race = _context.Races.Where(t => t.OwnerID == _userManager.GetUserId(User))
.FirstOrDefault(t => t.ID == id);
//var race = _context.Races.FirstOrDefault(t => t.ID == id);
if (race == null)
{
return NotFound();
}
race.OwnerID = _userManager.GetUserId(User);
race.Name = item.Name;
race.ContactName = item.ContactName;
race.ContactEmail = item.ContactEmail;
race.ContactNumber = item.ContactNumber;
race.RaceDate = item.RaceDate;
race.RaceStartTime = item.RaceStartTime;
race.Description = item.Description;
race.MaxEntries = item.MaxEntries;
race.CurrentEntries = item.CurrentEntries;
race.IsCompleted = item.IsCompleted;
race.IsPublished = item.IsPublished;
race.IsOpenForEntries = item.IsOpenForEntries;
race.LastUpdated = DateTime.Now;
_context.Races.Update(race);
_context.SaveChanges();
return new NoContentResult();
}
/// <summary>
/// Delete a Race
/// </summary>
/// <remarks>
/// Deletes a Race. Note: This will orphan any related result data and is not recommended!
/// </remarks>
/// <param name="id">Race ID</param>
/// <returns></returns>
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var race = _context.Races.Where(p => p.OwnerID == _userManager.GetUserId(User)).FirstOrDefault(t => t.ID == id);
//var race = _context.Races.FirstOrDefault(t => t.Id == id);
if (race == null)
{
return NotFound();
}
var raceid = race.ID;
////Delete associated race entries
//foreach (var raceentry in _context.RaceEntries.Where(p => p.OwnerId == User.GetUserId())
// .Where(p => p.RaceID == raceid))
//{
// _context.RaceEntries.Remove(raceentry);
//}
////Delete associated race timings
//foreach (var timing in _context.Timings.Where(p => p.OwnerId == User.GetUserId())
// .Where(p => p.RaceId == raceid))
//{
// _context.Timings.Remove(timing);
//}
//Delete/Save the deletion of the race
_context.SaveChanges();
return new NoContentResult();
}
}
}
I had the same issue and on any Authorize request, i was getting redirected to /account/login. I found the solution to add the Schemes for authentication.
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpPost]
public async Task<IActionResult> Like([FromBody]int contentId)
{
var userId = await UserId();
return Json(_content.IsLiked(contentId, true, userId));
}
The same code does not work without the
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
As Shawn Wildermuth says in his blog:
Note that we’re specifying which schemes to use. The Cookies and JwtBearer both have default scheme names so unless we’re renamed the scheme (which we could do in Startup.cs), we can just use the scheme name to tell the API to use JWT only, not cookies at all.
If we try again after this, it works with a JWT Token only. If you did want to support both (but don’t), the property AuthenticationSchemes takes a comma delimited list of scheme names.
So, you need to specify on :
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
both of the schemes:
[Authorize(AuthenticationSchemes = "Identity.Application,"+JwtBearerDefaults.AuthenticationScheme)]
Hope this can help you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With