How to retrieve ClaimsPrincipal from JWT in asp.net core
Asked Answered
I

2

13

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\":\"...

Instill answered 30/3, 2017 at 21:0 Comment(0)
H
6

First of all, I am not even sure whether this retrieving of ClaimsPrinciapal from JWT is a right approach or not.

It's likely not the approach I'd personally use. Instead, I'd simply rely on the JWT middleware to extract the ClaimsPrincipal from the access token for me (no need to manually use JwtSecurityTokenHandler for that).

The exception thrown by IdentityModel is actually caused by a very simple root cause: you've configured OpenIddict to use an ephemeral RSA asymmetric signing key (via AddEphemeralSigningKey()) and registered a symmetric signing key in the JWT bearer options, a scenario that can't obviously work.

You have two options to fix that:

  • Register your symmetric signing key in the OpenIddict options using AddSigningKey(SigningKey) so OpenIddict can use it.

  • Use an asymmetric signing key (by calling AddEphemeralSigningKey(), or AddSigningCertificate()/AddSigningKey() on production) and let the JWT bearer middleware use it instead of your symmetric signing key. For that, remove the entire TokenValidationParameters configuration to allow IdentityModel to download the public signing key from OpenIddict's discovery endpoint.

Hestia answered 3/4, 2017 at 5:54 Comment(2)
Thanks @PinPoint. The first option worked. Will also try second option but as of right now, going ahead with the first one.Instill
Sorry for the downvote. I must've done that unintentionally wanting to upvote and now I can't change it unless the post is edited.Swim
H
34

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.

Holly answered 23/12, 2020 at 15:58 Comment(2)
That's why I ended up here and your post solved my question. Not sure if claims is the best option for jwt yet.Codex
This was exactly what I needed to do when implementing a custom JWT Refresh Token Rotation solution. Thank you.Ascent
H
6

First of all, I am not even sure whether this retrieving of ClaimsPrinciapal from JWT is a right approach or not.

It's likely not the approach I'd personally use. Instead, I'd simply rely on the JWT middleware to extract the ClaimsPrincipal from the access token for me (no need to manually use JwtSecurityTokenHandler for that).

The exception thrown by IdentityModel is actually caused by a very simple root cause: you've configured OpenIddict to use an ephemeral RSA asymmetric signing key (via AddEphemeralSigningKey()) and registered a symmetric signing key in the JWT bearer options, a scenario that can't obviously work.

You have two options to fix that:

  • Register your symmetric signing key in the OpenIddict options using AddSigningKey(SigningKey) so OpenIddict can use it.

  • Use an asymmetric signing key (by calling AddEphemeralSigningKey(), or AddSigningCertificate()/AddSigningKey() on production) and let the JWT bearer middleware use it instead of your symmetric signing key. For that, remove the entire TokenValidationParameters configuration to allow IdentityModel to download the public signing key from OpenIddict's discovery endpoint.

Hestia answered 3/4, 2017 at 5:54 Comment(2)
Thanks @PinPoint. The first option worked. Will also try second option but as of right now, going ahead with the first one.Instill
Sorry for the downvote. I must've done that unintentionally wanting to upvote and now I can't change it unless the post is edited.Swim

© 2022 - 2024 — McMap. All rights reserved.