我正在创建一个Hosted Blazor WASM应用程序,该应用程序连接到Duende IdentityServer应用程序进行身份验证和授权。Blazor服务器部分作为BFF运行。
为用户分配了一个角色,并且该角色具有权限。我想将这些权限添加为声明,这样我就可以在Blazor应用程序中使用它进行授权。我将权限添加到";许可证ClaimsPrincipalFactory";。在";PermissionClaimProfileService";我正在将用户的所有声明添加到上下文中。已发行的索赔。
当我点击";登录";在Blazor应用程序中,我被重定向到IdentityServer应用程序上的登录页面。登录后,我被重定向到Blazor,我看到分配给我的用户的声明。但我在个人资料服务中添加的声明并没有显示在该页面上。
在我的日志中,我可以看到我的配置文件服务正在返回所有索赔。但我看不到他们在客户端。
我试了好几件事,但我看不出我做错了什么。
这就是我在日志中看到的:
配置文件服务返回了以下声明类型:subpreferred_username amr auth_time idp电子邮件AspNet.Identity.SecurityStamphttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier租户密钥权限电子邮件验证
这些是浏览器中显示的声明:
amr
pwd
sid
5BB444DB7141399BFA6ED4D1A65B1AE6
sub
5c009dae-d118-44e1-9a46-c119dcc31ff9
auth_time
1662538078
idp
local
name
{Name}
email
{Email}
bff:logout_url
/bff/logout?sid=5BB444DB7141399BFA6ED4D1A65B1AE6
bff:session_expires_in
1209598
bff:session_state
EvCMn29HkOmdDzxlAqvwUGfj3u0DgxHCymQtn1tOw0U.4F818675DFBEC4B689B4E3159633443A
Blazor应用程序Uri:https://localhost:7111IdentityServer Uri:https://localhost:7193
IdentityServer客户端配置
"BlazorClient": {
"AlwaysSendClientClaims": true,
"ClientId": "app-blazor",
"ClientSecrets": [
"SuperSecretPassword"
],
"ClientName": "App",
"ClientUri": "https://localhost:7111/",
"AllowedGrantTypes": [
"authorization_code"
],
"AllowOfflineAccess": true,
"RedirectUris": [
"https://localhost:7111/signin-oidc"
],
"FrontChannelLogoutUri": "https://localhost:7111/signout-oidc",
"PostLogoutRedirectUris": [
"https://localhost:7111/signout-callback-oidc"
],
"AllowedScopes": [
"openid",
"profile",
"api1"
]
}
Identity Server资源
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "MyAPI")
};
Identity Server配置文件服务
public class PermissionClaimProfileService : ProfileService<ApplicationUser>
{
public PermissionClaimProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
ILogger<ProfileService<ApplicationUser>> logger) : base(userManager, claimsFactory, logger)
{
}
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
await base.GetProfileDataAsync(context);
var existingClaims = context.IssuedClaims;
foreach(var claim in context.Subject.Claims)
{
if (!existingClaims.Select(c => c.Type).ToList().Contains(claim.Type))
existingClaims.Add(claim);
}
context.IssuedClaims = existingClaims.ToList();
}
}
Identity Server声明主体工厂
public class PermissionClaimsPrincipalFactory<TIdentityUser> : UserClaimsPrincipalFactory<TIdentityUser>
where TIdentityUser : IdentityUser
{
private readonly IClaimsCalculator _claimsCalculator;
/// <summary>
/// Needs UserManager and IdentityOptions, plus the two services to provide the permissions and dataKey
/// </summary>
/// <param name="userManager"></param>
/// <param name="optionsAccessor"></param>
/// <param name="claimsCalculator"></param>
public PermissionClaimsPrincipalFactory(UserManager<TIdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor,
IClaimsCalculator claimsCalculator)
: base(userManager, optionsAccessor)
{
_claimsCalculator = claimsCalculator;
}
/// <summary>
/// This adds the permissions and, optionally, a multi-tenant DataKey to the claims
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TIdentityUser user)
{
var identity = await base.GenerateClaimsAsync(user);
var userId = identity.Claims.GetUserIdFromClaims();
var claims = await _claimsCalculator.GetClaimsForUser(userId);
identity.AddClaims(claims);
return identity;
}
}
IdentityServer注册
services.AddScoped<IClaimsCalculator, ClaimsCalculator>();
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, PermissionClaimsPrincipalFactory<ApplicationUser>>();
services
.AddIdentityServer(options =>
{
options.KeyManagement.Enabled = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/
options.EmitStaticAudienceClaim = true;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("identityserver"), sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("identityserver"), sql => sql.MigrationsAssembly(migrationsAssembly));
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 1800;
})
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<PermissionClaimProfileService>();
Blazor服务器启动代码
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var configuration = builder.Configuration;
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddBff();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("cookie", options =>
{
options.Cookie.Name = "__Host-blazor";
options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = configuration.GetValue<string>("IdentityServer:Authority");
options.ClientId = configuration.GetValue<string>("IdentityServer:ClientId");
options.ClientSecret = configuration.GetValue<string>("IdentityServer:ClientSecret");
options.ResponseType = "code";
options.ResponseMode = "query";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api1");
options.MapInboundClaims = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/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.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.MapBffManagementEndpoints();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
}
Blazor客户展示索赔
<AuthorizeView>
<Authorized>
<dl>
@foreach (var claim in @context.User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
@if(context.User.HasPermission(Permissions.CompanyProfile_ManagePermissions)){
<p>User has permissions to manage permissions for company profile</p>
}
@if (context.User.HasPermission(Permissions.Property_Read))
{
<p>User has permissions to read properties</p>
}
</Authorized>
<NotAuthorized>
<h3>No session</h3>
</NotAuthorized>
</AuthorizeView>
通过这个问题的相关答案,我找到了解决方案。至少我现在得到了索赔。。。
这个答案让我想到:在使用identityserver 4.Net core 2.0 的客户端中,无法访问自定义声明
AlwaysIncludeUserClaimsInIdToken = true