Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to retrieve ClaimsPrincipal from JWT in asp.net core

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

like image 394
Nishith Shah Avatar asked Mar 30 '17 21:03

Nishith Shah


2 Answers

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.

like image 169
cdie Avatar answered Nov 10 '22 05:11

cdie


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.

like image 45
Kévin Chalet Avatar answered Nov 10 '22 04:11

Kévin Chalet