Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

ASP.Net Core Application Redirects to Cookie-based Login with JWT Authentication in Header #12842

Closed
kalittles opened this issue Aug 2, 2019 · 8 comments
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-identity Includes: Identity and providers

Comments

@kalittles
Copy link

kalittles commented Aug 2, 2019

I've implemented JWT and I have an endpoint secured with JWT.
The problem is that whenever I don't have JWT set as the default method of authentication, sending a JWT in the header will result in an HTTP response 200 that has a redirect to the login page of my website. The website is scaffolded using ASP.Net Identity in ASP.Net Core 2.2. I need it to be able to work with a mobile app sending JWTs, and the website. Setting the default authentication scheme to JWT breaks the website (can no longer log in via cookies).

If I make the endpoint that's supposed to use JWT AllowAnonymous, it functions, but it will not be able to claim the identity of the person sending the token (obviously).

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

It returns error 401 or the login redirect page depending on which JWT tutorial I use.

I've tried including

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

On every method that's supposed to take JWT.

I've tried adding

services.AddAuthentication( options =>
    {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }
)

To startup.

I've tried

            //services.ConfigureApplicationCookie(options =>
            //{
            //    options.Events.OnRedirectToLogin = context =>
            //    {
            //        context.Response.StatusCode = 401;
            //        return Task.CompletedTask;
            //    };
            //});

But that only returns a 401 when I submit a JWT in the header.

For the record, I'm testing using Postman and the header is

Authorization Bearer[WHITESPACE]token.

This is how I'm generating tokens and registering devices


        [HttpPost]
        [AllowAnonymous]
        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(_config["Tokens:Key"]));
                        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                        var token = new JwtSecurityToken(_config["Tokens:Issuer"],
                          _config["Tokens:Issuer"],
                          claims,
                          expires: DateTime.Now.AddMinutes(30),
                          signingCredentials: creds);

                        return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
                    }
                }
            }

            return BadRequest("Could not create token");
        }

        //[Produces("application/json")]
        //[Route("api/Test")]
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        public async Task<OkObjectResult> Get()
        {
            //string UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            return Ok("Worked");
        }



        [HttpPost]
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        public async Task<string> RegisterDevice([FromBody] TokenViewModel tokenViewModel)
        {
            string user = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            // Look up device ID.
            // If it exists, delete it, refresh it with a new one.
            var deviceFromDb = _context.Devices.Where(e => e.UserToken == tokenViewModel.UserToken).ToList(); // Grab any devices with the same email
            if (deviceFromDb.Count() > 0)
            {
                return tokenViewModel.UserToken;
            }
            else
            {
                DateTime date = new DateTime();
                date = DateTime.UtcNow;
                Device deviceToAdd = new Device();
                deviceToAdd.DeviceToken = tokenViewModel.DeviceToken;
                var users = _context.Users.Where(u => u.NormalizedEmail == user);
                if (users.Count() > 0)
                    deviceToAdd.UserToken = users.First().Id.ToString();
                deviceToAdd.DateRegistered = date;
                
                deviceToAdd.Email = user.ToString();
                
                await _context.AddAsync(deviceToAdd);
                await _context.SaveChangesAsync();

                // Send to other client.

                return tokenViewModel.DeviceToken;
            }

        }

Note REGISTER DEVICE does not work if I do not have the authentication scheme by default JWT. Meaning for every identity request that takes place, I must use JWT. This means that it effectively breaks the identity scaffolding altogether for cookie-based authentication.
That is:

services.AddAuthentication( options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})

However, the other function works - it even sends it to firebase

 [AllowAnonymous]
        public async Task<OkObjectResult> MeetRequest(double latitude, double longitude, string recipientUserName)
        {
            // Process. User A makes request with info
            // 
            string user = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            Meet meet = new Meet();
            meet.Host = user;
            meet.Recipient = recipientUserName;
            meet.Latitude = latitude.ToString();
            meet.Longitude = longitude.ToString();
            var device = _context.Devices.Where(d => d.Email == recipientUserName);
            //var registrationToken = device.First().DeviceToken;
            var registrationToken = user;
            var message = new Message()
            {
                Data = new Dictionary<string, string>() {
                    { "User", recipientUserName },
                    { "time", DateTime.Now.ToString() },
                },
                Token = registrationToken,
                Notification = new Notification(),
            };
            message.Notification.Title = "Test";
            message.Notification.Body = "Did google seriously not implement this.";


            string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
            Console.WriteLine("Successfully sent message: " + response);

            await _context.Meets.AddAsync(meet);
            await _context.SaveChangesAsync();
            return new OkObjectResult(response);
        }

I expect to be able to label methods with JWT authorization and have it work. I've even tried setting the default scheme to JWT and then setting the authorization scheme of each method back to Cookie based, but it doesn't seem to be working with any consistency and breaks my entire login mechanism for the website when I do that.

This is an extremely frustrating issue - there are seven billion different articles addressing different approaches and no unified explanation for how it should be handled. I think it should be common sense that someone would scaffold an application using Identity, then need to add a mobile application to the application stack as the business scales.

I'm really regretting choosing ASP.Net Core for development because of this. Any suggestions for how to fix this, or to allow mobile app authentication in conjunction with ASP.Net Core Identity would be much appreciated.

@blowdart blowdart added area-identity Includes: Identity and providers area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer labels Aug 2, 2019
@blowdart
Copy link
Contributor

blowdart commented Aug 2, 2019

Having both cookies and JWT is supported, and what you're doing looks ok. However, can we get the full contents of your ConfigureServices() and Configure() functions from startup.cs (with any secrets removed) along with what version of ASP.NET Core that you're using.

Is the JWT bearer authentication putting anything in the logs?

@kalittles
Copy link
Author

Thanks for getting back to me so fast, @blowdart .

I'm targeting ASP.Net Core 2.2.
Here's my entire startup:


    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.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<ApplicationUser>().AddRoles<IdentityRole>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.Configure<IdentityOptions>(options =>
            {
                //Password Settings
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequiredLength = 6;
                options.Password.RequiredUniqueChars = 0;

                // Lockout Settings.
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.AllowedForNewUsers = true;
                
                // User settings.
                options.User.AllowedUserNameCharacters =
                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
                options.User.RequireUniqueEmail = false;
            });

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                //options.Cookie.HttpOnly = true;
                //options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
                //options.LoginPath = "/Identity/Account/#";
                //options.AccessDeniedPath = "/Identity/Account/AccessDenied";
                //options.SlidingExpiration = true;

                options.Events.OnRedirectToLogin = context => {
                    context.Response.Headers["Location"] = context.RedirectUri;
                    context.Response.StatusCode = 401;
                    return Task.CompletedTask;
                };
            });

            //services.ConfigureApplicationCookie(options =>
            //{
            //    options.Events.OnRedirectToLogin = context =>
            //    {
            //        context.Response.StatusCode = 401;
            //        return Task.CompletedTask;
            //    };
            //});

            services.AddAuthentication(options =>
           {
               //options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
               //options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
           })
                .AddCookie(cfg => cfg.SlidingExpiration = true)
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidIssuer = Configuration["Tokens:Issuer"],
                        ValidAudience = Configuration["Tokens:Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
                    };
                });

            services.AddMvc(config =>
           {
               var policy = new AuthorizationPolicyBuilder()
                                   .RequireAuthenticatedUser()
                                   .Build();
               config.Filters.Add(new AuthorizeFilter(policy));
           }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddSignalR();

            // Hosted services
            services.AddHostedService<ProductDecayService>();

            // Authorization handlers.
            services.AddScoped<IAuthorizationHandler,
                                  ContactIsOwnerAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  ContactAdministratorsAuthorizationHandler>();

            services.AddSingleton<IAuthorizationHandler,
                                  ContactManagerAuthorizationHandler>();

            services.AddTransient<ICalculator, Calculator>();

            services.AddTransient<ITransformer, Transformer>();

            services.AddMvc().AddControllersAsServices();
            var nConfig = new NLog.Config.LoggingConfiguration();

            var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "nlogfile.txt" };
            var logconsole = new NLog.Targets.ConsoleTarget("logconsole");

            nConfig.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
            nConfig.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);

            NLog.LogManager.Configuration = nConfig;
        }

        // 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.UseForwardedHeaders(new ForwardedHeadersOptions
                {
                    ForwardedHeaders = ForwardedHeaders.XForwardedFor,

                    // IIS is also tagging a X-Forwarded-For header on, so we need to increase this limit, 
                    // otherwise the X-Forwarded-For we are passing along from the browser will be ignored
                    ForwardLimit = 2
                });
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                //app.UseBrowserLink();

            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseSignalR(routes =>
            {
                routes.MapHub<ChatHub>("/chatHub");
            });

            app.UseAuthentication();

            FirebaseApp.Create(new AppOptions
            {
                ProjectId = "``````-````````",
                Credential = GoogleCredential.FromFile("``````-``````-firebase-adminsdk-f4xwr-663088c49c.json"),
            });

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

I do not believe I'm using those AuthorizationHandlers (contactisowner/contact), I just followed a tutorial from a long time ago and forgot to remove the dead code.

@blowdart
Copy link
Contributor

blowdart commented Aug 2, 2019

OK for starters you can remove

                .AddCookie(cfg => cfg.SlidingExpiration = true)

because AddIdentity adds cookies anyway.

What I have working is the following;

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddDefaultIdentity<IdentityUser>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddAuthentication()
                .AddJwtBearer(options =>
                {
                    options.Audience = JwtParameters.Audience;
                    options.ClaimsIssuer = JwtParameters.Issuer;
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(JwtParameters.SigningKey),
                        ValidateIssuer = false,
                        ValidateAudience = true
                    };
                });


            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

I have a controller action which issues a JWT and then I'm using fiddler to send it, as an Authorization: Bearer header to a controller which looks like this;

    [Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public class JwtController : ControllerBase
    {
        // GET: api/Jwt
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "Hello" };
        }
    }

Can you strip everything down to just this sort of thing?

I've put the pieces I have working at https://github.com/blowdart/CookieJwtBearerSample

@kalittles
Copy link
Author

kalittles commented Aug 2, 2019

Silly question, but when I implement:

[Route("api/[controller]")]
    [ApiController]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public class JwtController : ControllerBase
    {
        // GET: api/Jwt
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "Hello" };
        }
    }

I make a request via Postman to
https://localhost:44346/api/jwts/get
And it doesn't return anything. Is my path incorrect? I figured it'd be the same as my other controllers.

edit: I figured it out, haha.

Also, I've stripped the entire startup section to this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Market.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Market.Areas.Hubs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Market.Areas.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Market.Areas.Geography.Interfaces;
using Market.Areas.Geography;
using Market.Areas.Services;
using Market.Models;
using Market.Areas.Imaging;
using Market.Areas.Imaging.Interfaces;
using NLog;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using FirebaseAdmin;
using Google.Apis.Auth.OAuth2;

namespace Market
{
    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.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<ApplicationUser>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>();

            //services.Configure<IdentityOptions>(options =>
            //{
            //    //Password Settings
            //    options.Password.RequireDigit = true;
            //    options.Password.RequireLowercase = true;
            //    options.Password.RequireNonAlphanumeric = true;
            //    options.Password.RequireUppercase = true;
            //    options.Password.RequiredLength = 6;
            //    options.Password.RequiredUniqueChars = 0;

            //    // Lockout Settings.
            //    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
            //    options.Lockout.MaxFailedAccessAttempts = 5;
            //    options.Lockout.AllowedForNewUsers = true;

            //    // User settings.
            //    options.User.AllowedUserNameCharacters =
            //    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
            //    options.User.RequireUniqueEmail = false;
            //});

            //services.ConfigureApplicationCookie(options =>
            //{
            //    // Cookie settings
            //    //options.Cookie.HttpOnly = true;
            //    //options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
            //    //options.LoginPath = "/Identity/Account/#";
            //    //options.AccessDeniedPath = "/Identity/Account/AccessDenied";
            //    //options.SlidingExpiration = true;

            //    options.Events.OnRedirectToLogin = context => {
            //        context.Response.Headers["Location"] = context.RedirectUri;
            //        context.Response.StatusCode = 401;
            //        return Task.CompletedTask;
            //    };
            //});

            //services.ConfigureApplicationCookie(options =>
            //{
            //    options.Events.OnRedirectToLogin = context =>
            //    {
            //        context.Response.StatusCode = 401;
            //        return Task.CompletedTask;
            //    };
            //});

            // services.AddAuthentication(options =>
            //{
            //    //options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            //    //options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            //})
            //     .AddCookie()
            //     .AddJwtBearer(cfg =>
            //     {
            //         cfg.RequireHttpsMetadata = false;
            //         cfg.SaveToken = true;
            //         cfg.TokenValidationParameters = new TokenValidationParameters()
            //         {
            //             ValidIssuer = Configuration["Tokens:Issuer"],
            //             ValidAudience = Configuration["Tokens:Issuer"],
            //             IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
            //         };
            //     });
            services.AddAuthentication()
               .AddJwtBearer(options =>
               {
                   options.Audience = JwtParameters.Audience;
                   options.ClaimsIssuer = JwtParameters.Issuer;
                   options.TokenValidationParameters = new TokenValidationParameters
                   {
                       ValidateIssuerSigningKey = true,
                       IssuerSigningKey = new SymmetricSecurityKey(JwtParameters.SigningKey),
                       ValidateIssuer = false,
                       ValidateAudience = true
                   };
               });


            
            services.AddSignalR();

            // Hosted services
            services.AddHostedService<ProductDecayService>();

            // Authorization handlers.
            //services.AddScoped<IAuthorizationHandler,
            //                      ContactIsOwnerAuthorizationHandler>();

            //services.AddSingleton<IAuthorizationHandler,
            //                      ContactAdministratorsAuthorizationHandler>();

            //services.AddSingleton<IAuthorizationHandler,
            //                      ContactManagerAuthorizationHandler>();

            services.AddTransient<ICalculator, Calculator>();

            services.AddTransient<ITransformer, Transformer>();

            services.AddMvc().AddControllersAsServices();
            var nConfig = new NLog.Config.LoggingConfiguration();

            var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "nlogfile.txt" };
            var logconsole = new NLog.Targets.ConsoleTarget("logconsole");

            nConfig.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
            nConfig.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);

            NLog.LogManager.Configuration = nConfig;
            services.AddMvc(config =>
            {
                //var policy = new AuthorizationPolicyBuilder()
                //                    .RequireAuthenticatedUser()
                //                    .Build();
                //config.Filters.Add(new AuthorizeFilter(policy));
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // 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.UseForwardedHeaders(new ForwardedHeadersOptions
                {
                    ForwardedHeaders = ForwardedHeaders.XForwardedFor,

                    // IIS is also tagging a X-Forwarded-For header on, so we need to increase this limit, 
                    // otherwise the X-Forwarded-For we are passing along from the browser will be ignored
                    ForwardLimit = 2
                });
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                //app.UseBrowserLink();

            }
            else
            {
                app.UseHttpsRedirection();
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseSignalR(routes =>
            {
                routes.MapHub<ChatHub>("/chatHub");
            });


            FirebaseApp.Create(new AppOptions
            {
                ProjectId = "market-231221",
                Credential = GoogleCredential.FromFile("market-231221-firebase-adminsdk-f4xwr-663088c49c.json"),
            });
            app.UseAuthentication();

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

        }
    }
}

And the other JWT controllers are not changing. Should I change my original JWT controller implementation that goes into AppSettings for Tokens:Issuer? I noticed that I did not have that class "JWT Parameters" and had to create it - it seems to be different from what I have in my appsettings.sjon

In appsettings.json -

  "Tokens": {
    "Key": "``````````````",
    "Issuer": "https://localhost:44346/"
  },

I'll clone your repo and just run your project to see if I can get it working.

edit:
**I got your repo working - I'll try to replicate what you did entirely and remove my old JWT implementation. **

I'll update if I run into anything - thanks so much for the help so far.

edit:
Actually, in your example it requires me to login via the site - can I fit this to my solution without that?

@kalittles
Copy link
Author

So I've tried integrating it using my view model -

        public IActionResult Jwt([FromBody] LoginViewModel model)
        {
            var signingHandler = new JwtSecurityTokenHandler();
            var jwtToken = signingHandler.CreateJwtSecurityToken(
                JwtParameters.Issuer,
                JwtParameters.Audience,
                new ClaimsIdentity(User.Claims, "jwt"),
                notBefore: DateTime.UtcNow,
                expires: DateTime.UtcNow.Add(JwtParameters.ValidFor),
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(JwtParameters.SigningKey), SecurityAlgorithms.HmacSha256));

            return Content(jwtToken.RawData);
        }

It returns a JWT, and the JWT can hit an endpoint - the problem is that the JWT is not associated with the account that I logged into. I'm assuming that there's something I have to do with the JWT middleware to establish the identity of the person that logged in.

More specifically, I am passing this:

https://localhost:44346/Meets/MeetRequest

With a JSON request:

{
	"username" : "``````@gmail.com",
	"password" : "`````"
}

And it's returning a JWT successfully:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NjQ3ODM3MjYsImV4cCI6MTU2NDc4NDAyNiwiaWF0IjoxNTY0NzgzNzI2LCJpc3MiOiJsb2NhbGhvc3QiLCJhdWQiOiJsb2NhbGhvc3QifQ.KXvFtPrIQ3hK8qK7yGSCvtc1qIOhZhE8C34AxS8M9g8

The problem is when I post this to a protected endpoint,

            string user = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

Returns null.
Any information I could use to map these two things would be cool - I'm not sure where to start, because as I said the SO answers I had before weren't working.

@kalittles
Copy link
Author

I don't want to celebrate prematurely, but it looks like I've managed to take what you did and integrate it into my solution - for any soul that comes after me and wants to perform something similar, I was able to set up a controller that issues tokens after receiving JSON in the format of email, password like this:

 public async Task<IActionResult> Jwt([FromBody] LoginViewModel model)
        {

            if (ModelState.IsValid)
            {
                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 signingHandler = new JwtSecurityTokenHandler();
                            var jwtToken = signingHandler.CreateJwtSecurityToken(
                                JwtParameters.Issuer,
                                JwtParameters.Audience,
                                new ClaimsIdentity(claims, "jwt"),
                                notBefore: DateTime.UtcNow,
                                expires: DateTime.UtcNow.Add(JwtParameters.ValidFor),
                                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(JwtParameters.SigningKey), SecurityAlgorithms.HmacSha256));
                            //var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
                            //var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                            //var token = new JwtSecurityToken(_config["Tokens:Issuer"],
                            //  _config["Tokens:Issuer"],
                            //  claims,
                            //  expires: DateTime.Now.AddMinutes(30),
                            //  signingCredentials: creds);

                            //return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
                            return Content(jwtToken.RawData);
                        }
                        return BadRequest("Could not create token");

                    }
                    return BadRequest("Could not create token");

                }

            }
 
            return BadRequest("Could not create token");


        }

When it receives something like this:

{
	"username" : "``````@gmail.com",
	"password" : "`````"
}

It returns a JWT related to the user that was sent in. This stops you from having to log in through a web page via browser.

I then set up a controller like this:

 [Authorize(AuthenticationSchemes =
            JwtBearerDefaults.AuthenticationScheme)]
        public async Task<OkObjectResult> MeetRequest(double latitude, double longitude, string recipientUserName)
        {
            // Process. User A makes request with info
            // 
            string user = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            Meet meet = new Meet();
            meet.Host = user;
            meet.Recipient = recipientUserName;
            meet.Latitude = latitude.ToString();
            meet.Longitude = longitude.ToString();
            var device = _context.Devices.Where(d => d.Email == recipientUserName);
            //var registrationToken = device.First().DeviceToken;
            var registrationToken = device.First().DeviceToken;
            var message = new Message()
            {
                Data = new Dictionary<string, string>() {
                    { "User", recipientUserName },
                    { "time", DateTime.Now.ToString() },
                },
                Token = registrationToken,
                Notification = new Notification(),
            };
            message.Notification.Title = "Test";
            message.Notification.Body = "Did google seriously not implement this.";


            string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
            Console.WriteLine("Successfully sent message: " + response);

            await _context.Meets.AddAsync(meet);
            await _context.SaveChangesAsync();
            return new OkObjectResult(response);
        }

That accepts the Authorization Bearer[WHITESPACE]Token header on request. It successfully looks up the intended user, finds the device registration token, and sends a request to Google FireCloud Messaging. The reason I did this was to allow integration for the website and also a mobile ios/android app that will need access to the same resources.

I don't want to close the issue prematurely, so I'll try to get the devices working over the weekend and update everyone. If it works, I'll try to contribute to a Medium article or something explaining @blowdart 's solution and how it might help people that make ASP.Net Core applications + Flutter applications.

Thanks so much again!
If I can leave a review or something for you I'd love to :D this saved me a ton of time.

@blowdart
Copy link
Contributor

blowdart commented Aug 3, 2019

The JWT auih middleware only supports it as a bearer token. If you want it from the body and attached to the user property on the request you need to write your own auth service for that.

@kalittles
Copy link
Author

I got it working - thanks a lot!

@ghost ghost locked as resolved and limited conversation to collaborators Dec 2, 2019
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer area-identity Includes: Identity and providers
Projects
None yet
Development

No branches or pull requests

2 participants