【问题标题】:How to access custom claim in aspnet core application authorized using Identity Server如何使用 Identity Server 访问 asp net core 应用程序中的自定义声明授权
【发布时间】:2020-07-24 10:43:10
【问题描述】:

我正在关注 Identity Server 快速入门模板,并尝试设置以下内容

  • 身份服务器 aspnet 核心应用程序
  • Mvc 客户端,向 is4 进行身份验证,并调用作为受保护 api 资源的 webapi 客户端。

ApplicationUser 有一个额外的列,我将其添加到来自ProfileService 的声明中,如下所示:

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            if (user == null)
                return;

            var principal = await _claimsFactory.CreateAsync(user);
            if (principal == null)
                return;

            var claims = principal.Claims.ToList();

            claims.Add(new Claim(type: "clientidentifier", user.ClientId ?? string.Empty));

            // ... add roles and so on

            context.IssuedClaims = claims;
        }

最后是 Mvc Client app ConfigureServices 方法中的配置:

            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc";
                options.ClientSecret = "mvc-secret";
                options.ResponseType = "code";

                options.SaveTokens = true;

                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("offline_access");

                options.Scope.Add("api1");

                options.GetClaimsFromUserInfoEndpoint = true;

                options.ClaimActions.MapUniqueJsonKey("clientidentifier", "clientidentifier");
            });

GetClaimsFromUserInfoEndpoint 设置为true 我可以访问User.Identity 中的自定义声明,但这会导致对ProfileService 的两次调用。

如果我删除或设置为 false,则此声明仍然是 access_token 的一部分,但不是 id_token 的一部分,然后我无法从上下文用户访问此特定声明。

有没有更好的方法可以从用户主体访问此声明而不会导致 2 次调用(就像现在一样)?或者也许从上下文中读取 access_token 并在检索到令牌后更新用户声明?

谢谢:)

【问题讨论】:

  • @RuardvanElburg 不是真的。在问这个问题之前我看到了那个线程。一个理想的解决方案是只调用一次端点,而不是根据调用者修改响应。我期待某种中间件或事件,这些声明将从 access_token 中获取并放入 id 令牌,或任何其他线索,了解其他人如何做到这一点
  • 这不是它的工作原理。 IdentityServer 发出令牌,并且对于每个令牌,使用不同的上下文调用该方法。您可以将客户端配置为仅请求访问令牌,但不能将此令牌更改为身份令牌。如果两者都需要,则需要两次调用。
  • 如果是这样的话,我会保持这样,因为我需要它们。

标签: c# asp.net-core identityserver4


【解决方案1】:

如果您想在客户端访问自定义声明而不是身份服务器中添加的声明,只需按照以下步骤操作,它对我有用。我想您在 asp.net 核心中将客户端和身份服务器作为单独的项目实现并且它们已经准备就绪,您现在想要玩声明或者可能想要通过角色声明等进行授权,好吧,让我们开始吧

  1. 创建一个继承自“IClaimsTransformation”的类,如下所示:
public class MyClaimsTransformation : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            var userName = principal.Identity.Name;
            var clone = principal.Clone();
            var newIdentity = (ClaimsIdentity)clone.Identity;
            var user = config.GetTestUsers().Where(p => p.Username == userName).First();
            if (user != null)
            {
                var lstUserClaims = user.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
                foreach (var item in lstUserClaims)
                    if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
                        newIdentity.AddClaim(item);
            }
            return Task.FromResult(principal);
        }
    }

但请注意,此类将通过用户身份验证多次调用,因此我添加了一个简单的代码来防止多次重复声明。你也有经过身份验证的用户的用户名。

  1. 接下来像这样创建另一个类:
public class ProfileService : IProfileService
    {
        //private readonly UserManager<ApplicationUser> userManager;

        public ProfileService(/*UserManager<ApplicationUser> userManager*/ /*, SignInManager<ApplicationUser> signInManager*/)
        {
            //this.userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            context.AddRequestedClaims(context.Subject.Claims);

            var collection = context.Subject.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
            foreach (var item in collection)
            {
                var lst = context.IssuedClaims.Where(p => p.Value == item.Value).ToList();
                if (lst.Count == 0)
                    context.IssuedClaims.Add(item);
            }

            await Task.CompletedTask;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            //context.IsActive = true;
            await Task.FromResult(0); /*Task.CompletedTask;*/
        }
    }

此类将通过多个上下文调用,但没关系,因为我们在此代码的第 1 部分添加了自定义声明

foreach (var item in lstUserClaims)
   if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
        newIdentity.AddClaim(item);
  1. 这是您在身份服务器端的基本 startup.cs:
public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options => options.EnableEndpointRouting = false);
            services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
            services.AddIdentityServer().AddDeveloperSigningCredential()
                    .AddInMemoryApiResources(config.GetApiResources())
                    .AddInMemoryIdentityResources(config.GetIdentityResources())
                    .AddInMemoryClients(config.GetClients())
                    .AddTestUsers(config.GetTestUsers())
                    .AddInMemoryApiScopes(config.GetApiScope())
                    .AddProfileService<ProfileService>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();

        }
    }

关注.AddProfileService&lt;ProfileService&gt;();services.AddTransient&lt;Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation&gt;();

  1. 现在在客户端转到 startup.cs 并执行以下操作:
.AddOpenIdConnect("oidc", options =>
{
   //other code
   options.GetClaimsFromUserInfoEndpoint = true;
   options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
})

对于我的示例,我尝试使用“角色”并通过我的自定义角色授权用户。

  1. 接下来在您的控制器类中执行以下操作: [Authorize(Roles = "myCustomClaimValue")] 或者您可以为自定义授权过滤器创建一个类。

请注意,您在身份服务器项目的配置文件中定义了测试用户,并且该用户有一个像 new claim(JwtClaimTypes.Role, "myCustomClaimValue") 这样的自定义声明,这将返回到 lstUserClaims 变量。

【讨论】:

    【解决方案2】:

    这里有一些关于这个主题的额外信息。默认情况下,IdentityServer 不会在身份令牌中包含身份声明。通过将客户端配置上的 AlwaysIncludeUserClaimsInIdToken 设置设置为 true 来允许。但不建议这样做。初始身份令牌通过表单发布或 URI 通过前端通道通信从授权端点返回。如果它通过 URI 返回并且令牌变得太大,您可能会遇到 URI 长度限制,这仍然取决于浏览器。大多数现代浏览器不存在长 URI 问题,但像 Internet Explorer 这样的旧浏览器可能会出现问题。这可能会或可能不会让您担心。看起来我的项目和你的很相似。祝你好运。

    【讨论】:

    • 是的,你对 url 长度限制是正确的,我会保持原样,最好明确地得到它们然后在某个地方失败:P 无论如何谢谢 :)
    【解决方案3】:

    原来身份服务器中的Client 对象具有完成这项工作的这个属性:

            //
            // Summary:
            //     When requesting both an id token and access token, should the user claims always
            //     be added to the id token instead of requring the client to use the userinfo endpoint.
            //     Defaults to false.
            public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
    

    正如 lib 元数据中解释的那样,为客户端将其设置为 true,那么客户端没有必要去从端点重新获取声明

    谢谢大家:)

    【讨论】:

      【解决方案4】:

      我假设您在调用 API 时将 Authorization 标头与 Bearer JWT 令牌一起传递。您可以在 API 控制器中从 HttpContext 读取 access_token。

        var accessToken = await this.HttpContext.GetTokenAsync("access_token");
          var handler = new JwtSecurityTokenHandler();
      
          if (handler.ReadToken(accessToken) is JwtSecurityToken jt && (jsonToken.Claims.FirstOrDefault(claim => claim.Type == "sub") != null))
          {
              var subID = jt.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
          }
      

      注意:GetClaimsFromUserInfoEndpoint 无需显式设置。

      【讨论】:

      • 谢谢,这是我从 mvc 访问受保护资源的方式,它可以工作,但我试图从上下文用户那里获得这个声明,声明主体如 User.Identity.Claims mvc
      猜你喜欢
      • 1970-01-01
      • 2020-02-01
      • 2019-07-31
      • 1970-01-01
      • 2018-10-17
      • 2021-02-16
      • 2019-08-15
      • 2018-10-18
      • 2017-03-20
      相关资源
      最近更新 更多