Search code examples
c#asp.net-coreasp.net-identity

How to manually validate JWT based on username in the url asp.net core


I am trying to use asp.net identity framework for mvc and JWT for APIs. Requirement is that api accessing username/device is in the url, for example, api/v1/username/accounts. The user or the device that JWT was issues has username in it. Can I do it in the startup.cs file. The following code was working fine until recently then it started doing strange thing by allowing asp.net identity to use JWT protected APIs. I want to check if username in the url api/v1/username/accounts matches the token one .Following is my code. Thanks for your insights.

 public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)

            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)

            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)

            .AddEnvironmentVariables();
             Configuration = builder.Build();

        Log.Logger = new LoggerConfiguration()

            .MinimumLevel
            .Warning()
            .WriteTo.RollingFile("Logs/GateKeeperLog-{Date}.txt")
            .CreateLogger();
    }

    public static IConfigurationRoot Configuration { get; set; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(Configuration);
        services.AddDbContext<GkEnterpriseContext>(options =>
        options.UseSqlServer(Configuration["Database:Connection"]));
        services.AddTransient<IUnitOfWork, UnitOfWork>();
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<GkEnterpriseContext>()
            .AddDefaultTokenProviders();
        services.AddMvc()
            .AddJsonOptions(options =>
            {
                options.SerializerSettings.Formatting = Formatting.Indented;
            }).AddJsonOptions(options =>
            {
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;

            });

        services.AddTransient<IEmailSender, AuthMessageSender>();
        services.AddTransient<ISmsSender, AuthMessageSender>();
    }

    // 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)
    {
        loggerFactory.AddSerilog();
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

         app.UseWhen(context => context.Request.Path.Value.Contains("/api")

        , builder =>
        {
            builder.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = Configuration["Tokens:Issuer"],
                    ValidAudience = Configuration["Tokens:Audidence"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes
                        (JwtTokenIssuer.PrivateKey)),
                    ValidateLifetime = true,

                    NameClaimType = JwtRegisteredClaimNames.FamilyName
                }

            });

            app.UseWhen(context => context.Request.Path.Value.StartsWith("/api/v2/computers/")

                , builder1 =>

                    builder1.MapWhen((ctx) =>
                    {
                        var deviceName = ctx.User.Claims.SingleOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value ?? "";
                        var testPath = new Microsoft.AspNetCore.Http.PathString($"/api/v2/computers/{deviceName}");

                        var pathMatch = ctx.Request.Path.StartsWithSegments(testPath);
                        return String.IsNullOrWhiteSpace(deviceName) || !pathMatch;

                    }, cfg =>
                    {
                        cfg.Run((req) =>
                        {
                            req.Response.StatusCode = 403;
                            return req.Response.WriteAsync("Sorry , you cant access this resource...");
                        });
                    }));


        });


        app.UseIdentity();
        app.UseStatusCodePagesWithReExecute("/StatusCodes/{0}");
        app.UseStaticFiles();


        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
            routes.MapRoute(
                name: "defaultApi",
                template: "api/v2/{controller}/{id?}");
        });




    }
}

// JWT issung code block, it is now issuing tokens as expected, only validating is the problem.

 var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub,computer),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.FamilyName,"GkDevice")
            };
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(PrivateKey));

            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: Startup.Configuration["Tokens:Issuer"],
                audience: Startup.Configuration["Tokens:Audidence"],
                claims: claims,
                notBefore: DateTime.Now,
                expires: DateTime.Now.AddYears(10),
                signingCredentials: creds
            );

            var data = new Token
            {
                Message = "New Token was issued",
                Jwt = new JwtSecurityTokenHandler().WriteToken(token),
                Iat = GkHelpers.ConvertTimeToEpoch(token.ValidFrom) ,
                Exp = GkHelpers.ConvertTimeToEpoch(token.ValidTo)
            };
            return data;

Solution

  • Something like this might help you --

    app.UseWhen(context => context.Request.Path.Value.StartsWith("/api"), builder =>
    {
        ...jwt code...
        builder.MapWhen((ctx) =>
        {
            var userName = ctx.User.Claims.SingleOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value ?? "";
            var testPath = new Microsoft.AspNetCore.Http.PathString($"/api/v2/computers/{userName}/");
            var pathMatch = ctx.Request.Path.StartsWithSegments(testPath);
            return String.IsNullOrWhiteSpace(userName) || !pathMatch;
        }, cfg =>
        {
            cfg.Run((req) =>
            {
                req.Response.StatusCode = 403;
                return req.Response.WriteAsync("");
            });
        });
    });
    

    The inner MapWhen will trigger when the username in the "Name" claim (configure how to get the username here) does not match the given Path. It will then immediately execute the following request pipeline which will return a 403 code with an empty response body.

    I am unsure, however, if you can process Identity-related items in the same request pipeline in which you actually add identity. You might have to extract that MapWhen code outside of the UseWhen.