使用 IdentityServer4 和 Keycloak 18 进行基于角色的身份验证

Role based auth with IdentityServer4 and Keycloak 18

更新到 keycloak 18 后,它期望 id_token_hint 等于 id_token 和 sign-out 上的 post_logout_redirect_uri。 对于 .net 客户端,我们的 keycloak 管理员创建了新的客户端范围 - csharp-roles 将角色转换为 .net 的方便视图,并具有包括角色声明到 id_token 的设置。 我们禁用了此设置,因为 sign-out url 太大(id_token 包含角色声明)并且 sign-out 完美运行。 但是在此更改之后我们遇到登录问题,IdentityServer 在将令牌转换为 Identity 期间不会为其分配声明,我 猜测 它发生是因为 IdentityServer for MVC 项目读取来自 id_token 的所有声明,请查看下面我的设置

 services
            .AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
            {
                options.LoginPath = "/Login/Index";
                options.LogoutPath = "/Login/Logout";
                options.Events = new CookieAuthenticationEvents
                {
                    OnValidatePrincipal = async cookieContext =>
                    {
                        var validator = cookieContext.HttpContext.RequestServices
                            .GetRequiredService<IPrincipalValidator>();
                        if (!await validator.ValidatePrincipal(cookieContext.Principal.Identity as ClaimsIdentity))
                        {
                            //this will force Challenge redirect
                            cookieContext.RejectPrincipal();

                            //this will delete Identity Cookie, preventing issue with SessionController which relies on User.Identity.IsAuthenticated
                            await cookieContext.HttpContext.SignOutAsync(CookieAuthenticationDefaults
                                .AuthenticationScheme);
                        }
                    }
                };
            })
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.MetadataAddress = Configuration["oidc:Metadata"];
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.ClientId = Configuration["oidc:ClientId"];
                options.ClientSecret = Configuration["oidc:ClientSecret"];
                options.ResponseType = OpenIdConnectResponseType.Code;
                options.CallbackPath = "/oidc-callback";
                options.GetClaimsFromUserInfoEndpoint = true;
                options.Scope.Add("openid");
                options.Scope.Add("email");
                options.Scope.Add("profile");
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    ValidateAudience = false,
                    RoleClaimType =
                        "roles", // using short name here, this is needed because we replace role claims (see code below, OnTicketReceived event)
                };
                options.SaveTokens = false; //all tokens are stored in session by TokenSourceHelper
                options.Events = new OpenIdConnectEvents
                {
                    OnAuthorizationCodeReceived = context =>
                    {
                        // pass current session ID to keycloak so it can use if for backchannel requests
                        if (context.HttpContext.Session != null)
                        {
                            var sessionId = context.HttpContext.Session.GetUniqueSessionId();
                            context.TokenEndpointRequest.Parameters.Add(
                                TokenSourceHelper.KeycloakSessionStateParameter, sessionId);
                            //also reset the session ID if it was previously revoked
                            var logoutSessions = context.HttpContext.RequestServices
                                .GetRequiredService<ILogoutSessionManager>();
                            logoutSessions.Remove(null, sessionId);
                        }

                        return Task.CompletedTask;
                    },
                    //OnTokenValidated = 
                    OnTokenResponseReceived = context =>
                    {
                        var tokenSource =
                            context.HttpContext.RequestServices.GetRequiredService<ITokenSourceHelper>();
                        tokenSource.SetTokens(context.TokenEndpointResponse.AccessToken,
                            context.TokenEndpointResponse.RefreshToken, context.TokenEndpointResponse.IdToken);
                        return Task.CompletedTask;
                    },
                    OnTicketReceived = async context =>
                    {
                        var tokenSource =
                            context.HttpContext.RequestServices.GetRequiredService<ITokenSourceHelper>();
                        var accessToken = await tokenSource.GetToken();
                        var handler = new JwtSecurityTokenHandler();
                        var jwtSecurityToken = handler.ReadJwtToken(accessToken);
                        
                        //reduce the ticket size: shorten claims length
                        var identity = (ClaimsIdentity)context.Principal?.Identity;
                        var roles = jwtSecurityToken.Claims.Where(x => x.Type == identity.RoleClaimType);
                        identity.AddClaims(roles);

                    },
                    OnRemoteFailure = context =>
                    {
                        //no need to log the exception, it is logged by Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler
                        context.Response.Redirect("/Home");
                        context.HandleResponse();
                        return Task.CompletedTask;
                    },
                    OnRedirectToIdentityProviderForSignOut = async context =>
                    {
                        if(context.Properties.Items.ContainsKey(WebTime.Business.DTO.Common.Core.Enums.GeneralConstants.TokenHintKey))
                        {
                            var idToken =
                                context.Properties.Items[
                                    GeneralConstants.TokenHintKey];
                            context.ProtocolMessage.IdTokenHint = idToken;
                        }
                        await Task.CompletedTask;
                    }
                };
            })

如您所见,我强制从 access_token 读取角色,现在它可以正常工作,但我的 headers 太长了,因为我的 cookie 有 5大块。

也许还有另一种设置可以帮助 access_token 的真实角色声明? 为什么它从 id_token?

读取角色

提前致谢。

您使用 AddOpenIdConnect 的客户端不需要 peek/look 访问令牌。

客户端应该只关心id token,如果你想减小会话cookie的大小,那么你可以在IdentityServer中设置AlwaysIncludeUserClaimsInIdToken=false。然后,因为你已经设置 options.GetClaimsFromUserInfoEndpoint = true;然后 AddOpenIdConnect 处理程序将从 UserInfo 端点获取剩余的用户详细信息。

Keycloak 发送具有“角色”声明类型名称的角色,默认情况下 asp.net 核心身份不期望此名称,我使用 options.GetClaimsFromUserInfoEndpoint = true; 添加唯一映射:options.ClaimActions.MapUniqueJsonKey(ClaimsIdentity.DefaultRoleClaimType, "roles"); 然后已解析的角色数组,显示为字符串数组