简体   繁体   中英

.NET Core 3.0 using Identity + JWT fails to authorize

I am using Microsoft.AspNetCore.Identity.EntityFrameworkCore and I can generate JWT token and it works fine. However, I cannot go to any route that needs authentication after adding the token to the header like this: 'Authorization', 'Bearer: <token>' . I think there is something wrong with the way I login the user.

I appreciate any help or hint.

This is the startup.cs code:

    /// <summary>
    /// This method gets called by the runtime. Use this method to add services to the container.
    /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // If environment is localhost, then enable CORS policy, otherwise no cross-origin access
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy", builder => builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());
        });

        // Add framework services
        // Add functionality to inject IOptions<T>
        services.AddOptions();
        services.Configure<JwtSettings>(_configuration.GetSection("JwtSettings"));

        // Add our Config object so it can be injected
        services.Configure<SecureHeadersMiddlewareConfiguration>(
            _configuration.GetSection("SecureHeadersMiddlewareConfiguration"));

        services.AddLogging();

        services.AddRouting(options => options.LowercaseUrls = true);

        if (_env.IsDevelopment())
        {
            services.AddDistributedMemoryCache();
        }
        else
        {
            services.AddDistributedRedisCache(opt =>
                opt.Configuration = _configuration.GetRequiredValue<string>("REDISCLOUD_URL"));
        }

        services.AddSession(options =>
        {
            // Set a short timeout for easy testing.
            options.IdleTimeout = TimeSpan.FromMinutes(50);
            options.Cookie.HttpOnly = true;
            options.Cookie.Name = ApiConstants.ApplicationName;
            options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
        });

        services.AddDbContext<EntityDbContext>(builder =>
        {
            if (_env.IsDevelopment())
            {
                builder.UseSqlite(_configuration.GetValue<string>("ConnectionStrings:Sqlite"));
            }
            else
            {
                builder.UseNpgsql(
                    ConnectionStringUrlToResource(_configuration.GetRequiredValue<string>("DATABASE_URL")));
            }
        });

        services.AddIdentity<User, UserRole>(opt => opt.User.RequireUniqueEmail = true)
            .AddEntityFrameworkStores<EntityDbContext>()
            .AddDefaultTokenProviders();

        var jwtSetting = _configuration
            .GetSection("JwtSettings")
            .Get<JwtSettings>();

        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(config =>
            {
                config.RequireHttpsMetadata = false;
                config.SaveToken = true;

                config.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = jwtSetting.Issuer,
                    ValidAudience = jwtSetting.Audience,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.Key))
                };
            });

        services.AddControllers(opt =>
            {
                opt.EnableEndpointRouting = false;

                opt.ModelValidatorProviders.Clear();

                // Not need to have https
                opt.RequireHttpsPermanent = false;

                // Allow anonymous for localhost
                if (_env.IsDevelopment())
                {
                    opt.Filters.Add<AllowAnonymousFilter>();
                }

                opt.Filters.Add<CustomExceptionFilterAttribute>();
                opt.Filters.Add<FileUploadActionFilterAttribute>();
            })
            .AddNewtonsoftJson(option => option.SerializerSettings.Converters.Add(new StringEnumConverter()));

        services.AddSwaggerGen(config =>
        {
            config.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "Contractor-Finder-Api",
                Description = "Contractor finder service API layer, .NET Core + PostgresSQL"
            });

            // Set the comments path for the Swagger JSON and UI.
            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);

            if (File.Exists(xmlPath))
            {
                config.IncludeXmlComments(xmlPath);
            }

            config.OperationFilter<FileUploadOperation>();

            config.AddSecurityDefinition("Bearer", // Name the security scheme
                new OpenApiSecurityScheme
                {
                    Description = "JWT Authorization header using the Bearer scheme.",
                    Type = SecuritySchemeType.Http, // We set the scheme type to http since we're using bearer authentication
                    Scheme = "bearer" // The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer".
                });
        });

        var container = new Container(config =>
        {
            var (accessKeyId, secretAccessKey, url) = (
                _configuration.GetRequiredValue<string>("CLOUDCUBE_ACCESS_KEY_ID"),
                _configuration.GetRequiredValue<string>("CLOUDCUBE_SECRET_ACCESS_KEY"),
                _configuration.GetRequiredValue<string>("CLOUDCUBE_URL")
            );

            var prefix = new Uri(url).Segments[1];
            const string bucketName = "cloud-cube";

            // Generally bad practice
            var credentials = new BasicAWSCredentials(accessKeyId, secretAccessKey);

            // Create S3 client
            config.For<IAmazonS3>().Use(() => new AmazonS3Client(credentials, RegionEndpoint.USEast1));
            config.For<S3ServiceConfig>().Use(new S3ServiceConfig(bucketName, prefix));

            // Register stuff in container, using the StructureMap APIs...
            config.Scan(_ =>
            {
                _.AssemblyContainingType(typeof(Startup));
                _.Assembly("Logic");
                _.Assembly("Dal");
                _.WithDefaultConventions();
            });

            // Populate the container using the service collection
            config.Populate(services);

            config.For<IMapper>().Use(ctx => ResolveMapper(ctx, Assembly.Load("Logic"))).Singleton();
        });

        container.AssertConfigurationIsValid();

        return container.GetInstance<IServiceProvider>();
    }

    /// <summary>
    /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    /// </summary>
    /// <param name="app"></param>
    public void Configure(IApplicationBuilder app)
    {
        // Add SecureHeadersMiddleware to the pipeline
        app.UseSecureHeadersMiddleware(_configuration.Get<SecureHeadersMiddlewareConfiguration>());

        app.UseCors("CorsPolicy")
            .UseEnableRequestRewind()
            .UseDeveloperExceptionPage();

        if (_env.IsDevelopment())
        {
            app.UseDatabaseErrorPage();

            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 
            // specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); });
        }

        // Not necessary for this workshop but useful when running behind kubernetes
        app.UseForwardedHeaders(new ForwardedHeadersOptions
        {
            // Read and use headers coming from reverse proxy: X-Forwarded-For X-Forwarded-Proto
            // This is particularly important so that HttpContent.Request.Scheme will be correct behind a SSL terminating proxy
            ForwardedHeaders = ForwardedHeaders.XForwardedFor |
                               ForwardedHeaders.XForwardedProto
        });

        // Use wwwroot folder as default static path
        app.UseDefaultFiles()
            .UseStaticFiles()
            .UseCookiePolicy()
            .UseSession()
            .UseRouting()
            .UseAuthentication()
            .UseAuthorization()
            .UseEndpoints(endpoints => endpoints.MapControllers());

        Console.WriteLine("Application Started!");
    }
}

}

And finally this is my AccountController.cs :

namespace Api.Controllers
{
    [Route("api/[controller]")]
    public class AccountController : Controller
    {
        private readonly UserManager<User> _userManager;
        private readonly SignInManager<User> _signManager;
        private readonly IOptions<JwtSettings> _jwtSettings;
        private readonly RoleManager<UserRole> _roleManager;

        public AccountController(IOptions<JwtSettings> jwtSettings, UserManager<User> userManager,
            SignInManager<User> signManager, RoleManager<UserRole> roleManager)
        {
            _jwtSettings = jwtSettings;
            _userManager = userManager;
            _signManager = signManager;
            _roleManager = roleManager;
        }

        [HttpGet]
        [Route("")]
        [SwaggerOperation("AccountInfo")]
        public async Task<IActionResult> Index()
        {
            return User.Identity.IsAuthenticated
                ? Ok(await _userManager.FindByEmailAsync(User.Identity.Name))
                : Ok(new { });
        }

        [HttpPost]
        [Route("Register/{role}")]
        [SwaggerOperation("Register")]
        public async Task<IActionResult> Register([FromRoute] RoleEnum role,
            [FromBody] RegisterViewModel registerViewModel)
        {
            var user = UserFactory.New(role, x =>
            {
                x.Firstname = registerViewModel.Firstname;
                x.Lastname = registerViewModel.Lastname;
                x.Email = registerViewModel.Email;
                x.UserName = registerViewModel.Username;
                x.Role = role;
            });

            // Create user
            var identityResults = new List<IdentityResult>
            {
                await _userManager.CreateAsync(user, registerViewModel.Password)
            };

            // Create the role if not exist
            if (!await _roleManager.RoleExistsAsync(role.ToString()))
            {
                identityResults.Add(await _roleManager.CreateAsync(new UserRole {Name = role.ToString()}));
            }

            // Register the user to the role
            identityResults.Add(await _userManager.AddToRoleAsync(user, role.ToString()));

            return identityResults.Aggregate(true, (b, result) => b && result.Succeeded)
                ? (IActionResult) Ok("Successfully registered!")
                : BadRequest("Failed to register!");
        }

        [HttpPost]
        [Route("Login")]
        [SwaggerOperation("Login")]
        public async Task<IActionResult> Login([FromBody] LoginViewModel loginViewModel)
        {
            // Ensure the username and password is valid.
            var user = await _userManager.FindByNameAsync(loginViewModel.Username);

            if (user == null || !await _userManager.CheckPasswordAsync(user, loginViewModel.Password))
            {
                return BadRequest(new
                {
                    error = "", // OpenIdConnectConstants.Errors.InvalidGrant,
                    error_description = "The username or password is invalid."
                });
            }

            await _signManager.SignInAsync(user, true);

            // Generate and issue a JWT token
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, user.Email),
                new Claim(JwtRegisteredClaimNames.Sub, user.Email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Value.Key));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(
                _jwtSettings.Value.Issuer,
                _jwtSettings.Value.Issuer,
                claims,
                expires: DateTime.Now.AddMinutes(_jwtSettings.Value.AccessTokenDurationInMinutes),
                signingCredentials: credentials);

            var userRoleInfo = await _userManager.GetRolesAsync(user);

            return Ok(new
            {
                token = new JwtSecurityTokenHandler().WriteToken(token),
                roles = userRoleInfo,
                user.Role,
                user.Firstname,
                user.Lastname,
                user.Email
            });
        }

        [HttpPost]
        [Route("Logout")]
        [SwaggerOperation("Logout")]
        public async Task<IActionResult> Logout()
        {
            await _signManager.SignOutAsync();

            return Ok("Logged-Out");
        }
    }
}

This is an example of one of my controllers that I get not authorized error:

[Authorize(Roles = "Internal")]
[ApiController]
[Route("Api/[controller]")]
public class ProfileController : Controller
{
    private readonly UserManager<User> _userManager;

    public ProfileController(UserManager<User> userManager)
    {
        _userManager = userManager;
    }

    [HttpGet]
    [Route("")]
    public async Task<IActionResult> Index()
    {
        var user = await _userManager.FindByEmailAsync(User.Identity.Name);

        return Ok(new Profile(user));
    }
}

Screenshot of chrome dev tool:

在此处输入图片说明


Please correct your Authorization Header value.
try removing colon(:) after Bearer . 在此处输入图片说明

It should be only Bearer <Token>

For More Information -> https://tools.ietf.org/html/rfc6750#section-2.1

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM