In my solution, I have two projects. 1) Web API and 2) MVC. I am using ASP.NET Core. API issues JWT token and MVC consumes it to get protected resources. I am using openiddict library to issue JWT. In MVC project, in AccountController Login method, I want to retrieve ClaimsPrincipal (using JwtSecurityTokenHandler ValidateToken method) and assign to HttpContext.User.Claims and HttpContext.User.Identity. I want to store the token in session and for each request after successful login, pass it in header to Web API. I can successfully, issue JWT and consume it in MVC project, but when I try to retrieve ClaimsPrincipal it throws me an error. First of all, I am not even sure whether this retrieving of ClaimsPrinciapal from JWT is a right approach or not. And if it is, what is the way forward.
WebAPI.Startup.CS
public class Startup
{
public static string SecretKey => "MySecretKey";
public static SymmetricSecurityKey SigningKey => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IContainer ApplicationContainer { get; private set; }
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddCors();
services.AddMvc().AddJsonOptions(options => { options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver(); });
services.AddAutoMapper();
services.AddDbContext<MyDbContext>(options =>
{
options.UseMySql(Configuration.GetConnectionString("MyDbContext"));
options.UseOpenIddict();
});
services.AddOpenIddict(options =>
{
options.AddEntityFrameworkCoreStores<TelescopeContext>();
options.AddMvcBinders();
options.EnableTokenEndpoint("/Authorization/Token");
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.DisableHttpsRequirement();
options.UseJsonWebTokens();
options.AddEphemeralSigningKey();
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30));
});
var config = new MapperConfiguration(cfg => { cfg.AddProfile(new MappingProfile()); });
services.AddSingleton(sp => config.CreateMapper());
// Create the Autofac container builder.
var builder = new ContainerBuilder();
// Add any Autofac modules or registrations.
builder.RegisterModule(new AutofacModule());
// Populate the services.
builder.Populate(services);
// Build the container.
var container = builder.Build();
// Create and return the service provider.
return container.Resolve<IServiceProvider>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseCors(builder => builder.WithOrigins("http://localhost:9001/")
.AllowAnyOrigin());
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = "http://localhost:9001/",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "http://localhost:9000/",
RequireHttpsMetadata = false,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:9001/",
ValidateAudience = true,
ValidAudience = "http://localhost:9000",
ValidateLifetime = true,
IssuerSigningKey = SigningKey
}
});
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
applicationLifetime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}
}
WebAPI.AuthorizationController.cs which issues JWT.
[Route("[controller]")]
public class AuthorizationController : Controller
{
private IUsersService UserService { get; set; }
public AuthorizationController(IUsersService userService)
{
UserService = userService;
}
[HttpPost("Token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
if (await UserService.AuthenticateUserAsync(new ViewModels.AuthenticateUserVm() { UserName = request.Username, Password = request.Password }) == false)
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
var user = await UserService.FindByNameAsync(request.Username);
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme, OpenIdConnectConstants.Claims.Name, null);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, user.UserId.ToString(), OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Username, user.UserName, OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Email, user.EmailAddress, OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.GivenName, user.FirstName, OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.MiddleName, user.MiddleName, OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.FamilyName, user.LastName, OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.EmailVerified, user.IsEmailConfirmed.ToString(), OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Audience, "http://localhost:9000", OpenIdConnectConstants.Destinations.AccessToken);
var principal = new ClaimsPrincipal(identity);
return SignIn(principal, OpenIdConnectServerDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
}
MVC.AccountController.cs contains Login, GetTokenAsync method.
public class AccountController : Controller
{
public static string SecretKey => "MySecretKey";
public static SymmetricSecurityKey SigningKey => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginVm vm, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var token = await GetTokenAsync(vm);
SecurityToken validatedToken = null;
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:9001/",
ValidateAudience = true,
ValidAudience = "http://localhost:9000",
ValidateLifetime = true,
IssuerSigningKey = SigningKey
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
try
{
ClaimsPrincipal principal = handler.ValidateToken(token.AccessToken, validationParameters, out validatedToken);
}
catch (Exception e)
{
throw;
}
}
return View(vm);
}
private async Task<TokenVm> GetTokenAsync(LoginVm vm)
{
using (var client = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Post, $"http://localhost:9001/Authorization/Token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "password",
["username"] = vm.EmailAddress,
["password"] = vm.Password
});
var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadAsStringAsync();
//if (payload["error"] != null)
// throw new Exception("An error occurred while retriving an access tocken.");
return JsonConvert.DeserializeObject<TokenVm>(payload);
}
}
}
Error I am getting: "IDX10501: Signature validation failed. Unable to match 'kid': '0-AY7TPAUE2-ZVLUVQMMUJFJ54IMIB70E-XUSYIB', \ntoken: '{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"0-AY7TPAUE2-ZVLUVQMMUJFJ54IMIB70E-XUSYIB\"}.{\"sub\":\"10\",\"username\":\"...
See this thread because I was looking for the very same thing (not the exception though), and the accepted answer indeed helps the OP, however, it doesn't help me with : how to create ClaimsPrincipal
from JWT Token.
After some research and digging, I've found a way to do it manually (it was my case, I had to do it manually in a specific case).
To do so, first, parse the token with JwtSecurityTokenHandler
class :
var token = new JwtSecurityTokenHandler().ReadJwtToken(n.TokenEndpointResponse.AccessToken);
After that, you just ned to create a new ClaimsPrincipal :
var identity = new ClaimsPrincipal(new ClaimsIdentity(token.Claims));
In my specific case, I just have to update claims on my already authenticated user, so I use this code :
var identity = (ClaimsIdentity)User.Identity;
identity.AddClaims(token.Claims);
Hope it will help someone one day if looking after the answer for the title.