Asp.Net Identity 2 用户信息如何映射到 IdentityServer3 配置文件声明
How does Asp.Net Identity 2 User Info get mapped to IdentityServer3 profile claims
我已经 Asp.Net Identity 2 全部设置好,运行 自定义用户存储由 SQL 服务器通过 Dapper 支持。此时,在我的 dev/testing 中,我只关心本地帐户(但会添加外部登录提供程序)。我有一个自定义用户,其中包含 Asp.Net Identity 想要的标准属性,并添加了一些我自己的(FirstName,LastName):
public class AppUser : IUser<Guid>
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string PasswordHash { get; set; }
public string SecurityStamp { get; set; }
public string Email { get; set; }
public bool EmailConfirmed { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset LockoutEndDate { get; set; }
public int AccessFailedCount { get; set; }
// Custom User Properties
public string FirstName { get; set; }
public string LastName { get; set; }
}
在我的 MVC 网络应用程序中,我像这样配置 OIDC:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.Authority"],
ClientId = "MVC.Web",
Scope = "openid profile email",
RedirectUri = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.RedirectUri"],
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
由于我将 profile
作为请求的范围,我得到:
preferred_username: testuser
并且由于我将 email
作为请求的范围,我得到:
email: user@test.com
email_verified: true
我没有明确告诉我的 AspNetIdentityUserService
如何将我的 AppUser
中的 UserName
属性 映射到 preferred_username
声明,我是不知道那是怎么发生的。 因此,我不明白如何将 FirstName
属性 映射到 given_name
声明,以便它将与 id_token
一起返回。
我研究的内容:
因此,如果您查看 IdentityServer3 AspNetIdentity sample here,我发现这个 ClaimsIdentityFactory
看起来应该可以解决问题:
public override async Task<ClaimsIdentity> CreateAsync(UserManager<User, string> manager, User user, string authenticationType)
{
var ci = await base.CreateAsync(manager, user, authenticationType);
if (!String.IsNullOrWhiteSpace(user.FirstName))
{
ci.AddClaim(new Claim("given_name", user.FirstName));
}
if (!String.IsNullOrWhiteSpace(user.LastName))
{
ci.AddClaim(new Claim("family_name", user.LastName));
}
return ci;
}
所以我将其添加到我的应用程序中,并在我的自定义 UserManager
中进行了连接。当实例化 class 时,我确实遇到了一个断点,但我从来没有在 CreateAsync
方法上遇到过断点,我的声明也没有返回。
我也看到了这个 IdentityServer3 Custom User sample here,我发现这个 GetProfileDataAsync
方法看起来可能是正确的(但似乎我正在挖掘比我应该做的更深的东西)貌似是这样simple/common):
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// issue the claims for the user
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
if (user != null)
{
context.IssuedClaims = user.Claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
}
return Task.FromResult(0);
}
我在这里遇到了同样的问题,因为此方法中的断点从未被触发。我什至查看了 IdentityServer3 源代码,发现只有在作用域设置了 IncludeAllClaimsForUser
标志时才会调用它。但是我在这里使用的是标准 profile
范围,所以我开始质疑我是否需要为设置了 IncludAllClaimsForUser
标志的配置文件范围做出自己的定义,或者是否有办法将该标志添加到标准范围。
并添加到所有这些...这只需要在使用本地帐户时完成。当我实施外部登录提供商时,我会在那里询问配置文件,并希望能够获得名字和姓氏。那么我想知道一旦我已经得到这些声明会发生什么(或者如何确定我是否需要从我的用户存储中提取它们)。好像我需要连接到只在本地登录时运行的东西。
然后我开始真正质疑我是否以正确的方式进行此操作,因为我 seeing/finding 对此知之甚少(我本以为这是一个相当普遍的情况,其他人已经实施,预计会找到 docs/samples)。已经尝试解决这个问题一天了。希望有人有一个快速 answer/pointer!
我使用 OpenIdConnectAuthenticationNotifications
来实现这一点,您可以连接到 ASP.NET 身份数据库或在其中做任何事情,这是我在我的一个项目中使用的示例代码:
这是我 Startup.cs 的完整源代码,但您真正需要的只是 SecurityTokenValidated
部分 ...
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Helpers;
using IdentityServer3.Core;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
namespace MyProject
{
public partial class Startup
{
public static string AuthorizationServer => ConfigurationManager.AppSettings["security.idserver.Authority"];
public void ConfigureOAuth(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
jwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
SecurityTokenValidator = jwtSecurityTokenHandler,
Authority = AuthorizationServer,
ClientId = ConfigurationManager.AppSettings["security.idserver.clientId"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["security.idserver.postLogoutRedirectUri"],
RedirectUri = ConfigurationManager.AppSettings["security.idserver.redirectUri"],
ResponseType = ConfigurationManager.AppSettings["security.idserver.responseType"],
Scope = ConfigurationManager.AppSettings["security.idserver.scope"],
SignInAsAuthenticationType = "Cookies",
#if DEBUG
RequireHttpsMetadata = false, //not recommended in production
#endif
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
},
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
//// we want to keep first name, last name, subject and roles
//var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
//var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
//var sub = id.FindFirst(Constants.ClaimTypes.Subject);
//var roles = id.FindAll(Constants.ClaimTypes.Role);
//// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.Name,
Constants.ClaimTypes.Role);
nid.AddClaims(id.Claims);
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim("access_Token", n.ProtocolMessage.AccessToken));
////nid.AddClaim(givenName);
////nid.AddClaim(familyName);
////nid.AddClaim(sub);
////nid.AddClaims(roles);
////// add some other app specific claim
// Connect to you ASP.NET database for example
////nid.AddClaim(new Claim("app_specific", "some data"));
//// keep the id_token for logout
//nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
//app.UseResourceAuthorization(new AuthorizationManager());
}
}
}
这个问题的 (A) 正确答案是覆盖 AspNetIdentityUserService
class 中的 GetProfileDataAsync
方法,如下所示:
public class AppUserService : AspNetIdentityUserService<AppUser, Guid>
{
private AppUserManager _userManager;
public AppUserService(AppUserManager userManager)
: base(userManager)
{
_userManager = userManager;
}
public async override Task GetProfileDataAsync(ProfileDataRequestContext ctx)
{
var id = Guid.Empty;
if (Guid.TryParse(ctx.Subject.GetSubjectId(), out id))
{
var user = await _userManager.FindByIdAsync(id);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(Constants.ClaimTypes.PreferredUserName, user.UserName),
new Claim(Constants.ClaimTypes.Email, user.Email),
new Claim(Constants.ClaimTypes.GivenName, user.FirstName),
new Claim(Constants.ClaimTypes.FamilyName, user.LastName)
};
ctx.IssuedClaims = claims;
}
}
}
}
但正如我所发现的,这还不够。查看source code for IdentityServer,你会发现这个位:
if (scopes.IncludesAllClaimsForUserRule(ScopeType.Identity))
{
Logger.Info("All claims rule found - emitting all claims for user.");
var context = new ProfileDataRequestContext(
subject,
client,
Constants.ProfileDataCallers.ClaimsProviderIdentityToken);
await _users.GetProfileDataAsync(context);
var claims = FilterProtocolClaims(context.IssuedClaims);
if (claims != null)
{
outputClaims.AddRange(claims);
}
return outputClaims;
}
请注意 GetProfileDataAsync
不会被调用,除非设置了一个标志来包含所有声明(不确定他们为什么选择这样做,但显然必须有充分的理由!)。所以我认为这意味着我需要完全重新定义 profile
范围,但通过进一步挖掘源代码,我发现情况并非如此。 StandardScopes
has a method that creates the scopes with the always include flag set。而不是设置你的范围这样做:
factory.UseInMemoryScopes(StandardScopes.All);
这样做:
factory.UseInMemoryScopes(StandardScopes.AllAlwaysInclude);
那么您的 GetProfileDataAsync
将是 运行,您将获得所有索赔!
注意:我第一次尝试使用 ClaimsIdentityFactory
永远不会成功,因为我没有登录到 Asp.Net Identity,所以这永远不会成功除非那是我正在做的事情。
注意:如果您希望在已经从 Identity Server 收到 id_token 后添加声明(尤其是应用程序特定声明),@Rosdi Kasim 的回答肯定有效。
我已经 Asp.Net Identity 2 全部设置好,运行 自定义用户存储由 SQL 服务器通过 Dapper 支持。此时,在我的 dev/testing 中,我只关心本地帐户(但会添加外部登录提供程序)。我有一个自定义用户,其中包含 Asp.Net Identity 想要的标准属性,并添加了一些我自己的(FirstName,LastName):
public class AppUser : IUser<Guid>
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string PasswordHash { get; set; }
public string SecurityStamp { get; set; }
public string Email { get; set; }
public bool EmailConfirmed { get; set; }
public bool LockoutEnabled { get; set; }
public DateTimeOffset LockoutEndDate { get; set; }
public int AccessFailedCount { get; set; }
// Custom User Properties
public string FirstName { get; set; }
public string LastName { get; set; }
}
在我的 MVC 网络应用程序中,我像这样配置 OIDC:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.Authority"],
ClientId = "MVC.Web",
Scope = "openid profile email",
RedirectUri = ConfigurationManager.AppSettings["OpenIdConnectAuthenticationOptions.RedirectUri"],
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
由于我将 profile
作为请求的范围,我得到:
preferred_username: testuser
并且由于我将 email
作为请求的范围,我得到:
email: user@test.com
email_verified: true
我没有明确告诉我的 AspNetIdentityUserService
如何将我的 AppUser
中的 UserName
属性 映射到 preferred_username
声明,我是不知道那是怎么发生的。 因此,我不明白如何将 FirstName
属性 映射到 given_name
声明,以便它将与 id_token
一起返回。
我研究的内容:
因此,如果您查看 IdentityServer3 AspNetIdentity sample here,我发现这个 ClaimsIdentityFactory
看起来应该可以解决问题:
public override async Task<ClaimsIdentity> CreateAsync(UserManager<User, string> manager, User user, string authenticationType)
{
var ci = await base.CreateAsync(manager, user, authenticationType);
if (!String.IsNullOrWhiteSpace(user.FirstName))
{
ci.AddClaim(new Claim("given_name", user.FirstName));
}
if (!String.IsNullOrWhiteSpace(user.LastName))
{
ci.AddClaim(new Claim("family_name", user.LastName));
}
return ci;
}
所以我将其添加到我的应用程序中,并在我的自定义 UserManager
中进行了连接。当实例化 class 时,我确实遇到了一个断点,但我从来没有在 CreateAsync
方法上遇到过断点,我的声明也没有返回。
我也看到了这个 IdentityServer3 Custom User sample here,我发现这个 GetProfileDataAsync
方法看起来可能是正确的(但似乎我正在挖掘比我应该做的更深的东西)貌似是这样simple/common):
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// issue the claims for the user
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
if (user != null)
{
context.IssuedClaims = user.Claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
}
return Task.FromResult(0);
}
我在这里遇到了同样的问题,因为此方法中的断点从未被触发。我什至查看了 IdentityServer3 源代码,发现只有在作用域设置了 IncludeAllClaimsForUser
标志时才会调用它。但是我在这里使用的是标准 profile
范围,所以我开始质疑我是否需要为设置了 IncludAllClaimsForUser
标志的配置文件范围做出自己的定义,或者是否有办法将该标志添加到标准范围。
并添加到所有这些...这只需要在使用本地帐户时完成。当我实施外部登录提供商时,我会在那里询问配置文件,并希望能够获得名字和姓氏。那么我想知道一旦我已经得到这些声明会发生什么(或者如何确定我是否需要从我的用户存储中提取它们)。好像我需要连接到只在本地登录时运行的东西。
然后我开始真正质疑我是否以正确的方式进行此操作,因为我 seeing/finding 对此知之甚少(我本以为这是一个相当普遍的情况,其他人已经实施,预计会找到 docs/samples)。已经尝试解决这个问题一天了。希望有人有一个快速 answer/pointer!
我使用 OpenIdConnectAuthenticationNotifications
来实现这一点,您可以连接到 ASP.NET 身份数据库或在其中做任何事情,这是我在我的一个项目中使用的示例代码:
这是我 Startup.cs 的完整源代码,但您真正需要的只是 SecurityTokenValidated
部分 ...
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Helpers;
using IdentityServer3.Core;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
namespace MyProject
{
public partial class Startup
{
public static string AuthorizationServer => ConfigurationManager.AppSettings["security.idserver.Authority"];
public void ConfigureOAuth(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
jwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
SecurityTokenValidator = jwtSecurityTokenHandler,
Authority = AuthorizationServer,
ClientId = ConfigurationManager.AppSettings["security.idserver.clientId"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["security.idserver.postLogoutRedirectUri"],
RedirectUri = ConfigurationManager.AppSettings["security.idserver.redirectUri"],
ResponseType = ConfigurationManager.AppSettings["security.idserver.responseType"],
Scope = ConfigurationManager.AppSettings["security.idserver.scope"],
SignInAsAuthenticationType = "Cookies",
#if DEBUG
RequireHttpsMetadata = false, //not recommended in production
#endif
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
},
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
//// we want to keep first name, last name, subject and roles
//var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
//var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
//var sub = id.FindFirst(Constants.ClaimTypes.Subject);
//var roles = id.FindAll(Constants.ClaimTypes.Role);
//// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.Name,
Constants.ClaimTypes.Role);
nid.AddClaims(id.Claims);
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim("access_Token", n.ProtocolMessage.AccessToken));
////nid.AddClaim(givenName);
////nid.AddClaim(familyName);
////nid.AddClaim(sub);
////nid.AddClaims(roles);
////// add some other app specific claim
// Connect to you ASP.NET database for example
////nid.AddClaim(new Claim("app_specific", "some data"));
//// keep the id_token for logout
//nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
//app.UseResourceAuthorization(new AuthorizationManager());
}
}
}
这个问题的 (A) 正确答案是覆盖 AspNetIdentityUserService
class 中的 GetProfileDataAsync
方法,如下所示:
public class AppUserService : AspNetIdentityUserService<AppUser, Guid>
{
private AppUserManager _userManager;
public AppUserService(AppUserManager userManager)
: base(userManager)
{
_userManager = userManager;
}
public async override Task GetProfileDataAsync(ProfileDataRequestContext ctx)
{
var id = Guid.Empty;
if (Guid.TryParse(ctx.Subject.GetSubjectId(), out id))
{
var user = await _userManager.FindByIdAsync(id);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(Constants.ClaimTypes.PreferredUserName, user.UserName),
new Claim(Constants.ClaimTypes.Email, user.Email),
new Claim(Constants.ClaimTypes.GivenName, user.FirstName),
new Claim(Constants.ClaimTypes.FamilyName, user.LastName)
};
ctx.IssuedClaims = claims;
}
}
}
}
但正如我所发现的,这还不够。查看source code for IdentityServer,你会发现这个位:
if (scopes.IncludesAllClaimsForUserRule(ScopeType.Identity))
{
Logger.Info("All claims rule found - emitting all claims for user.");
var context = new ProfileDataRequestContext(
subject,
client,
Constants.ProfileDataCallers.ClaimsProviderIdentityToken);
await _users.GetProfileDataAsync(context);
var claims = FilterProtocolClaims(context.IssuedClaims);
if (claims != null)
{
outputClaims.AddRange(claims);
}
return outputClaims;
}
请注意 GetProfileDataAsync
不会被调用,除非设置了一个标志来包含所有声明(不确定他们为什么选择这样做,但显然必须有充分的理由!)。所以我认为这意味着我需要完全重新定义 profile
范围,但通过进一步挖掘源代码,我发现情况并非如此。 StandardScopes
has a method that creates the scopes with the always include flag set。而不是设置你的范围这样做:
factory.UseInMemoryScopes(StandardScopes.All);
这样做:
factory.UseInMemoryScopes(StandardScopes.AllAlwaysInclude);
那么您的 GetProfileDataAsync
将是 运行,您将获得所有索赔!
注意:我第一次尝试使用 ClaimsIdentityFactory
永远不会成功,因为我没有登录到 Asp.Net Identity,所以这永远不会成功除非那是我正在做的事情。
注意:如果您希望在已经从 Identity Server 收到 id_token 后添加声明(尤其是应用程序特定声明),@Rosdi Kasim 的回答肯定有效。