【问题标题】:MVC Client and Api working together in IdentityServer4MVC Client 和 Api 在 IdentityServer4 中协同工作
【发布时间】:2018-10-10 15:56:00
【问题描述】:

我正在使用 IdentityServer4 创建身份服务器,并且我有一个使用身份服务器安全性的 MVC 项目,但我还在同一个 MVC 项目中公开了一个 API 并且没有分开,我已阅读并遵循切换到的指示混合流并添加 API 访问,但我还没有得到它。 我需要的是让 MVC 客户端和 Api 在同一个项目上一起工作,两者都受到身份项目的安全性保护。这可能吗?

【问题讨论】:

  • 同一个项目还是同一个流程?
  • 我解决这种情况,将中间件配置为:
  • 制作了一个工作样本。诀窍是为 JwtBearer 添加身份验证处理程序并在 API 控制器中使用相同的方案。 Startup.cs: services.AddAuthentication() .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.Authority = "localhost:5000"; options.RequireHttpsMetadata = false; options.Audience = "api1"; options.SaveToken = true; }); API 控制器:[Route("[controller]")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 公共类 IdentityController : ControllerBase {

标签: c# identityserver4


【解决方案1】:

有关如何配置 ASP.NET MVC 应用程序的示例,请参阅 https://github.com/auth0-samples/aspnet-core-mvc-plus-webapi

基本上归结为:

  1. 添加 Cookie 身份验证,但对于 API 调用,返回 HTTP 401 而不是挑战。您需要为非 API 调用保留挑战。
  2. 添加 Jwt 不记名令牌认证(或使用 IdentityServer4.AccessTokenValidation)
  3. 使用[Authorize] 时指定AuthenticationScheme 控制器/操作的属性。当您想使用 cookie 身份验证时,您可能需要使用 AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,而当您想使用访问令牌时,您可能需要指定 AuthenticationScheme = JwtBearerDefaults.AuthenticationScheme

【讨论】:

  • 谢谢,我需要配置 IdentityServer 来证明链接中的客户端。
  • @PedroPabloFuentesPi 您可以为此使用隐式流程。只需将响应类型指定为id_token token。对于这种情况,您可能必须在 IdentityServer 配置中为您的客户端设置 AllowOfflineAccesstrue 以允许发送访问令牌。然后您可以使用access_token 执行API 调用。
【解决方案2】:

是的,我们可以用于安全辞职你可以观察到。还有其他方法

//folder position 
create folders to house our models, controllers, data, and migrations. It’s also recommended that you create a folder for APIs called Areas


Now that we have the folders set up, let’s start by creating our UserStore. We’ll need this in order to authenticate our users and generate tokens. One of the best ways to do this is to create a User class that extends from IdentityUser.
//Add models for identity server user

using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TokenAuthASPCore.Models
{
    public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

Also, let’s create a Todo class for testing our REST API later on.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TokenAuthASPCore.Models
{
    public class Todo
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public bool Finished { get; set; }
    }
}



Setting up our Database
In order to be able to use both of these, we must first connect to a database. I’ll be using MS SQL-Server.

Add a connection string to you appsettings.json file that specifies the location of the database that you want to interact with.

"ConnectionStrings": {
    "TestConnection": "Data Source=DESKTOP-GIIMG7O\\JAMES;Initial Catalog=IdentityBlog;Integrated Security=True"
  }

Then go to your Startup.cs file and add your configuration for your database connection.



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

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();



IdentityServer 4 Configuration
The next step is to configure IdentityServer4. IdentityServer4 is a framework that allows for us to add OIDC authentication and authorization to our APS.NET Core application. It allows for the generation of JWT tokens and supports many of the Oauth 2 flows. We’ll be using the ResourceOwnerPassword flow because, in later tutorials, we’ll be authentication with both a React and Angular app as well as a Xamarin application.

We’ll start off by creating a Config.cs file. This will hold most of our configuration for IdentityServer 4.




using IdentityServer4;
using IdentityServer4.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TokenAuthASPCore
{
    public class Config
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Email(),
                new IdentityResources.Profile(),
            };
        }


        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            // client credentials client
            return new List<Client>
            {
                
                // resource owner password grant client
                new Client
                {
                    ClientId = "ro.angular",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        IdentityServerConstants.StandardScopes.Address,
                        "api1"
                    }
                }
            };
        }
    }
}




IdentityResources represent what information we can get from our Identity Store, our API Resources represent the API information we can access and the Clients represent what clients are allowed to authenticate with our API.

If we jump back to our Startup.cs file, we can now register IdentityServer4 as a service. This is configured in your ConfigureServices method.


services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryPersistedGrants()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients())
                .AddAspNetIdentity<ApplicationUser>();

            services.AddMvc();


            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
                {
                    // base-address of your identityserver
                    options.Authority = "http://localhost:52718/";

                    // name of the API resource
                    options.Audience = "api1";

                    options.RequireHttpsMetadata = false;
                });

		//We’ll also add routing and IdentityServer to our HTTP pipeline.
		// Inside of the Configure method add
		app.UseIdentityServer();
            app.UseAuthentication();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "areas",
                    template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
                );
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });


User Registration
Now that we have Identity Server set up, we’ll have to create a way for our users to sign-up. Let’s start off by creating a view model for user registrations.  Then we’ll create an AccountController for user signup.


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace TokenAuthASPCore.Models.AccountViewModels
{
    public class RegisterViewModel
    {
        [Required]
        [StringLength(50, ErrorMessage = "The {0} must be at least {2}, at max {1} characters long and unique.", MinimumLength = 2)]
        [Display(Name = "Username")]
        public string UserName { get; set; }

        [Required]
        [StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }

        [Required]
        [StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}




using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using TokenAuthASPCore.Areas.v1.Models.UserViewModels;
using TokenAuthASPCore.Models;
using TokenAuthASPCore.Models.AccountViewModels;

namespace TokenAuthASPCore.Areas.Controllers
{
    [Area("v1")]
    public class AccountController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AccountController(
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager
            )
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

        [HttpPost]
        public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var user = new ApplicationUser { UserName = model.UserName, FirstName = model.FirstName, LastName = model.LastName, Email = model.Email};

            var result = await _userManager.CreateAsync(user, model.Password);

            string role = "Basic User";

            if (result.Succeeded)
            {
                if (await _roleManager.FindByNameAsync(role) == null)
                {
                    await _roleManager.CreateAsync(new IdentityRole(role));
                }
                await _userManager.AddToRoleAsync(user, role);
                await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("userName", user.UserName));
                await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("firstName", user.FirstName));
                await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("lastName", user.LastName));
                await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("email", user.Email));
                await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("role", role));

                return Ok(new ProfileViewModel(user));
            }

            return BadRequest(result.Errors);


        }
    }
}



Inside of the controller, we have a register method that takes in the data from a validated ViewModel, creates the user with a basic role, then adds claims to that user.

We’ll also create a ProfileViewModel creating a custom output of user data.




using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TokenAuthASPCore.Models;

namespace TokenAuthASPCore.Areas.Models.UserViewModels
{
    public class ProfileViewModel
    {
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }

        public ProfileViewModel()
        {

        }

        public ProfileViewModel(ApplicationUser user)
        {
            Id = user.Id;
            FirstName = user.FirstName;
            LastName = user.LastName;
            Email = user.Email;
        }

        public static IEnumerable<ProfileViewModel> GetUserProfiles(IEnumerable<ApplicationUser> users)
        {
            var profiles = new List<ProfileViewModel> { };
            foreach (ApplicationUser user in users)
            {
                profiles.Add(new ProfileViewModel(user));
            }

            return profiles;
        }
    }
}


Adding Claims To the JWT
In order the add Claims to the JWT token, you’re going to have to create a class that implements the IdentityServer4.Services.IProfileService interface. This is used to determine the profile data that is placed into the JWT.

public class IdentityClaimsProfileService : IProfileService
{

}Copy
Now that you have the class, you’ll have the implement both GetProfileDataAsync() and IsActiveAsync(). You’ll have to define a ClaimsFactory as well as the UserManager for getting your user information.

	private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;

        public IdentityClaimsProfileService(UserManager<ApplicationUser> userManager, 	IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
        {
            _userManager = userManager;
            _claimsFactory = claimsFactory;
        }

Once this is done, use the GetProfileDataAsync method to populate your JWT with the desired User information.

	public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            var principal = await _claimsFactory.CreateAsync(user);
            var roles = await _userManager.GetRolesAsync(user);
            var claims = principal.Claims.ToList();
            claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
            claims.Add(new Claim(JwtClaimTypes.GivenName, user.FirstName));
            claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email));
            foreach(string role in roles)
            {
                claims.Add(new Claim(JwtClaimTypes.Role, role));
            }
            
            context.IssuedClaims = claims;
        }

As you see here, I place the user’s email and firstname in the token. You can decode the JWT and use this inside of a Javascript or Native application to display the user info without having to do a constant fetch of user information.

Next, we’ll create an implementation of IsActiveAsync. This will check whether the user is valid in your database.

	public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
Now that this is done, now you have to configure your application to use your custom implementation of IProfileService.

Inside of you Statup.cs file, you must use Dependency Injection to specify this implementation.

services.AddTransient<IProfileService, IdentityClaimsProfileService>();



//Bellow is the Startup class


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();
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryPersistedGrants()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients())
                .AddAspNetIdentity<ApplicationUser>();

            services.AddMvc();


            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
                {
                    // base-address of your identityserver
                    options.Authority = "http://localhost:52718/";

                    // name of the API resource
                    options.Audience = "api1";

                    options.RequireHttpsMetadata = false;
                });

	services.AddTransient<IProfileService, IdentityClaimsProfileService>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

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

        app.UseStaticFiles();

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


//In main method of the proghrame.cs call the startup



public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-25
    • 2011-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-06
    • 2011-03-04
    • 1970-01-01
    相关资源
    最近更新 更多