如何使用自定义用户服务向 identityserver3 的访问令牌添加声明
How to add claims to access token for identityserver3 using custom user service
我正在尝试创建自定义用户,对其进行身份验证,然后 return 用户使用身份服务器 3 声明进入 angular 应用程序。我查看了样本,特别是 CustomUserService项目。
编辑我已根据进度将问题更新为:
在 starup.cs 文件中,我有范围和客户端加载
var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
//.UseInMemoryUsers(Users.Get());
然后UserServices.cs
中的覆盖
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
string hashedPassword = string.Empty;
string salt = string.Empty;
//pull users
using (IdentityModelContext ctx = new IdentityModelContext())
{
IIdSrvUserRepository ur = new IdSrvUserRepository(ctx);
Users = ur.GetAll().ToList();
}
//salt curent password
var user = Users.SingleOrDefault(x => x.UserName == context.UserName);
if (user != null)
{
salt = user.Salt;
hashedPassword = IqUtils.GenerateSaltedPassword(context.Password, salt);
if (user.UserName == context.UserName && hashedPassword == user.Password)
{
try
{
context.AuthenticateResult = new AuthenticateResult(user.Subject, user.UserName);
}
catch (Exception ex)
{
string msg = $"<-- Login Error: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
}
return Task.FromResult(0);
}
我遇到问题的地方是 GetProfileDataAsync 方法。我不知道如何向客户端添加声明,或者我是否需要调用第二个客户端。
该应用程序是向 Idsrv 进行身份验证的 angular 客户端。我创建了一个客户
//Angular client authentication
new Client
{
ClientId = "iqimplicit",
ClientName = "IQ Application (Implicit)",
Flow = Flows.Implicit,
AllowAccessToAllScopes = true,
IdentityTokenLifetime = 300,//default value is 5 minutes this is the token that allows a user to login should only be used once
AccessTokenLifetime = 3600, // default is one hour access token is used for securing routes and access to api in IQ
RequireConsent = false,
RequireSignOutPrompt = true,
AllowedScopes = new List<string>
{
//no clue if this is correct
StandardScopes.Profile.Name
},
RedirectUris = new List<string>
{
angularClient + "callback.html"
},
PostLogoutRedirectUris = new List<string>()
{
//redirect to login screen
angularClient
}
}
在 SO posting here 之后,我试图将声明添加到访问令牌 returned 到客户端。
我定义的范围只是针对资源。我假设我需要为身份创建第二个类似于注释掉的部分?
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.ProfileAlwaysInclude,
StandardScopes.Address,
new Scope
{
Name = "appmanagement",
DisplayName = "App Management",
Description = "Allow application to management.",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>()
{
new ScopeClaim("role", false),
new ScopeClaim("Name", true),
new ScopeClaim("GivenName", true),
new ScopeClaim("FamilyName", true),
new ScopeClaim("Email", true),
},
},
// new Scope
// {
// Name = "iquser",
// DisplayName = "User",
// Description = "Identifies the user",
// Type = ScopeType.Identity,
// Claims = new List<ScopeClaim>()
// {
// new ScopeClaim("Name", true)
// }
// }
};
}
}
}
我想作为声明传回的信息如上所示,(姓名、名字、姓氏电子邮件)。
试图遵循前面提到的 SO 帖子的评论,我覆盖了应该为用户获取信息的 GetProfileDataAsync
。
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)
{
if(context.RequestedClaimTypes != null)
try
{
if (context.RequestedClaimTypes != null)
{
List<Claim> newclaims = new List<Claim>();
foreach (Claim claim in context.Subject.Claims)
{
if (context.RequestedClaimTypes.Contains(claim.Type))
{
newclaims.Add(claim);
}
}
context.IssuedClaims = newclaims;
}
}
catch (Exception ex)
{
string msg = $"<-- Error Getting Profile: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
//return Task.FromResult(context.IssuedClaims);
return Task.FromResult(0);
}
}
这就是我卡住的地方 当我看到我在 context.RequestedClaimTypes
中定义的声明时 claim.Type
中从来没有匹配项,因为它始终包含 idsrv 声明的基本属性。
由于这是一个自定义数据库,所有关于索赔的信息都存储在数据库的用户 table 中,而不是索赔 table 中。我试图将用户 table 每个字段的值映射到 Claim 值中。
更新
我已经返回并从范围中取消注释并将以下声明添加到身份范围类型
new Scope
{
Name = "iuser",
DisplayName = "User",
Description = "Identifies the user",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>()
{
new ScopeClaim(Constants.ClaimTypes.Name, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.GivenName, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.FamilyName, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.Email, alwaysInclude: true),
}
}
初始身份验证有效,在回调中我可以看到评估的范围。但是,我仍然坚持现在如何从应用于这些范围的数据库中获取值。这部分代码(如预期的那样)正在寻找 System.Security.Claims.Claim
类型。现在这是确定如何获取声明值的停止点,这些声明值是我的用户应用的属性,并且 returned 作为声明。
此时我被重定向回 angular 应用程序,但 accesstoken 对象对于用户信息是空白的。
此时如何将自己的值插入到声明的 context.IssuedClaims
中?
提前致谢
乍一看,您的 angular 客户端似乎没有配置为请求资源范围 iuser
,您只配置了 name
身份范围的请求。将该范围添加到客户端 iqimplicit
的允许范围列表中,并确保将该范围添加到您的 angular 应用程序的客户端配置中。
经过更多的努力,我终于找到了一种添加声明的方法。
在 UserService
class 中 GetProfileDataAsync()
我最终能够使用以下方法创建和添加声明。
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
if (user != null)
{
try
{
//add subject as claim
var claims = new List<Claim>
{
new Claim(Constants.ClaimTypes.Subject, user.Subject),
};
//get username
UserClaim un = new UserClaim
{
Subject = Guid.NewGuid().ToString(),
ClaimType = "username",
ClaimValue = string.IsNullOrWhiteSpace(user.UserName) ? "unauth" : user.UserName
};
claims.Add(new Claim(un.ClaimType, un.ClaimValue));
//get email
UserClaim uem = new UserClaim
{
Subject = Guid.NewGuid().ToString(),
ClaimType = "email",
ClaimValue = string.IsNullOrWhiteSpace(user.EmailAddress) ? string.Empty : user.EmailAddress
};
context.IssuedClaims = claims;
}
catch (Exception ex)
{
string msg = $"<-- Error Getting Profile: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
//return Task.FromResult(context.IssuedClaims);
return Task.FromResult(0);
我遇到的问题更多是在模型中。通常保存在 Claims 存储中的所有信息都存储在用户本身,因此在创建配置文件时,此时我必须从用户那里提取值并将它们 create/add 作为声明。
然后,angular 客户端中的 OIDC 经理使用以下方式授予我对访问令牌及其值的声明的访问权限:
$scope.mgr.oidcClient.loadUserProfile($scope.mgr.access_token)
.then(function (userInfoValues) {
$scope.profile = userInfoValues;
$scope.fullName = $scope.profile.given_name + ' ' + $scope.profile.family_name;
checkAdmin($scope.profile.sys_admin);
$scope.$apply();
});
请注意,此处使用 $apply()
只是因为此代码存在于整个应用程序使用的指令中。
我不确定这是否是添加声明的最佳实施方式,但这种方法确实有效。暂时我会推迟将其标记为答案,看看是否有更好的方法。
关于您似乎正在尝试做的事情的一些评论。
1 您正在复制现有范围,即 profile
范围。
如果您想要的所有声明都在 OIDC 标准范围内(此处:profile
/ email
),则无需定义自己的范围 iuser
。您在 iuser
身份范围内声明的声明已包含在标准的 profile
范围内。因此,只需请求 profile
范围,并允许您的客户请求该范围。
标准范围,以及它们的声明内容是什么:
http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
2 你客户端配置上的AllowedScopes
配置有点不对
改用以下常量。
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
}
3 您还没有真正展示如何获取 id_tokens..
.. 但请记住向 idsrv 提供您想要 id_token 返回的请求,包括来自 profile
范围的声明。对 /authorize
、 /userinfo
或 /token
端点的请求必须换句话说包含一个 scope
参数,其值至少为 openid profile
.
例如,就像 Brock A. 在他的 oidc js 客户端库中所做的那样:
https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/JavaScriptImplicitClient/app.js#L18
我正在尝试创建自定义用户,对其进行身份验证,然后 return 用户使用身份服务器 3 声明进入 angular 应用程序。我查看了样本,特别是 CustomUserService项目。
编辑我已根据进度将问题更新为:
在 starup.cs 文件中,我有范围和客户端加载
var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
//.UseInMemoryUsers(Users.Get());
然后UserServices.cs
中的覆盖public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
string hashedPassword = string.Empty;
string salt = string.Empty;
//pull users
using (IdentityModelContext ctx = new IdentityModelContext())
{
IIdSrvUserRepository ur = new IdSrvUserRepository(ctx);
Users = ur.GetAll().ToList();
}
//salt curent password
var user = Users.SingleOrDefault(x => x.UserName == context.UserName);
if (user != null)
{
salt = user.Salt;
hashedPassword = IqUtils.GenerateSaltedPassword(context.Password, salt);
if (user.UserName == context.UserName && hashedPassword == user.Password)
{
try
{
context.AuthenticateResult = new AuthenticateResult(user.Subject, user.UserName);
}
catch (Exception ex)
{
string msg = $"<-- Login Error: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
}
return Task.FromResult(0);
}
我遇到问题的地方是 GetProfileDataAsync 方法。我不知道如何向客户端添加声明,或者我是否需要调用第二个客户端。
该应用程序是向 Idsrv 进行身份验证的 angular 客户端。我创建了一个客户
//Angular client authentication
new Client
{
ClientId = "iqimplicit",
ClientName = "IQ Application (Implicit)",
Flow = Flows.Implicit,
AllowAccessToAllScopes = true,
IdentityTokenLifetime = 300,//default value is 5 minutes this is the token that allows a user to login should only be used once
AccessTokenLifetime = 3600, // default is one hour access token is used for securing routes and access to api in IQ
RequireConsent = false,
RequireSignOutPrompt = true,
AllowedScopes = new List<string>
{
//no clue if this is correct
StandardScopes.Profile.Name
},
RedirectUris = new List<string>
{
angularClient + "callback.html"
},
PostLogoutRedirectUris = new List<string>()
{
//redirect to login screen
angularClient
}
}
在 SO posting here 之后,我试图将声明添加到访问令牌 returned 到客户端。
我定义的范围只是针对资源。我假设我需要为身份创建第二个类似于注释掉的部分?
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.ProfileAlwaysInclude,
StandardScopes.Address,
new Scope
{
Name = "appmanagement",
DisplayName = "App Management",
Description = "Allow application to management.",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>()
{
new ScopeClaim("role", false),
new ScopeClaim("Name", true),
new ScopeClaim("GivenName", true),
new ScopeClaim("FamilyName", true),
new ScopeClaim("Email", true),
},
},
// new Scope
// {
// Name = "iquser",
// DisplayName = "User",
// Description = "Identifies the user",
// Type = ScopeType.Identity,
// Claims = new List<ScopeClaim>()
// {
// new ScopeClaim("Name", true)
// }
// }
};
}
}
}
我想作为声明传回的信息如上所示,(姓名、名字、姓氏电子邮件)。
试图遵循前面提到的 SO 帖子的评论,我覆盖了应该为用户获取信息的 GetProfileDataAsync
。
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)
{
if(context.RequestedClaimTypes != null)
try
{
if (context.RequestedClaimTypes != null)
{
List<Claim> newclaims = new List<Claim>();
foreach (Claim claim in context.Subject.Claims)
{
if (context.RequestedClaimTypes.Contains(claim.Type))
{
newclaims.Add(claim);
}
}
context.IssuedClaims = newclaims;
}
}
catch (Exception ex)
{
string msg = $"<-- Error Getting Profile: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
//return Task.FromResult(context.IssuedClaims);
return Task.FromResult(0);
}
}
这就是我卡住的地方 当我看到我在 context.RequestedClaimTypes
中定义的声明时 claim.Type
中从来没有匹配项,因为它始终包含 idsrv 声明的基本属性。
由于这是一个自定义数据库,所有关于索赔的信息都存储在数据库的用户 table 中,而不是索赔 table 中。我试图将用户 table 每个字段的值映射到 Claim 值中。
更新 我已经返回并从范围中取消注释并将以下声明添加到身份范围类型
new Scope
{
Name = "iuser",
DisplayName = "User",
Description = "Identifies the user",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>()
{
new ScopeClaim(Constants.ClaimTypes.Name, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.GivenName, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.FamilyName, alwaysInclude: true),
new ScopeClaim(Constants.ClaimTypes.Email, alwaysInclude: true),
}
}
初始身份验证有效,在回调中我可以看到评估的范围。但是,我仍然坚持现在如何从应用于这些范围的数据库中获取值。这部分代码(如预期的那样)正在寻找 System.Security.Claims.Claim
类型。现在这是确定如何获取声明值的停止点,这些声明值是我的用户应用的属性,并且 returned 作为声明。
此时我被重定向回 angular 应用程序,但 accesstoken 对象对于用户信息是空白的。
此时如何将自己的值插入到声明的 context.IssuedClaims
中?
提前致谢
乍一看,您的 angular 客户端似乎没有配置为请求资源范围 iuser
,您只配置了 name
身份范围的请求。将该范围添加到客户端 iqimplicit
的允许范围列表中,并确保将该范围添加到您的 angular 应用程序的客户端配置中。
经过更多的努力,我终于找到了一种添加声明的方法。
在 UserService
class 中 GetProfileDataAsync()
我最终能够使用以下方法创建和添加声明。
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
if (user != null)
{
try
{
//add subject as claim
var claims = new List<Claim>
{
new Claim(Constants.ClaimTypes.Subject, user.Subject),
};
//get username
UserClaim un = new UserClaim
{
Subject = Guid.NewGuid().ToString(),
ClaimType = "username",
ClaimValue = string.IsNullOrWhiteSpace(user.UserName) ? "unauth" : user.UserName
};
claims.Add(new Claim(un.ClaimType, un.ClaimValue));
//get email
UserClaim uem = new UserClaim
{
Subject = Guid.NewGuid().ToString(),
ClaimType = "email",
ClaimValue = string.IsNullOrWhiteSpace(user.EmailAddress) ? string.Empty : user.EmailAddress
};
context.IssuedClaims = claims;
}
catch (Exception ex)
{
string msg = $"<-- Error Getting Profile: Message: {ex.Message}, Inner Exception:{ex.InnerException} --->";
myLogger log = new myLogger("IdentityServer");
}
}
//return Task.FromResult(context.IssuedClaims);
return Task.FromResult(0);
我遇到的问题更多是在模型中。通常保存在 Claims 存储中的所有信息都存储在用户本身,因此在创建配置文件时,此时我必须从用户那里提取值并将它们 create/add 作为声明。
然后,angular 客户端中的 OIDC 经理使用以下方式授予我对访问令牌及其值的声明的访问权限:
$scope.mgr.oidcClient.loadUserProfile($scope.mgr.access_token)
.then(function (userInfoValues) {
$scope.profile = userInfoValues;
$scope.fullName = $scope.profile.given_name + ' ' + $scope.profile.family_name;
checkAdmin($scope.profile.sys_admin);
$scope.$apply();
});
请注意,此处使用 $apply()
只是因为此代码存在于整个应用程序使用的指令中。
我不确定这是否是添加声明的最佳实施方式,但这种方法确实有效。暂时我会推迟将其标记为答案,看看是否有更好的方法。
关于您似乎正在尝试做的事情的一些评论。
1 您正在复制现有范围,即 profile
范围。
如果您想要的所有声明都在 OIDC 标准范围内(此处:profile
/ email
),则无需定义自己的范围 iuser
。您在 iuser
身份范围内声明的声明已包含在标准的 profile
范围内。因此,只需请求 profile
范围,并允许您的客户请求该范围。
标准范围,以及它们的声明内容是什么: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
2 你客户端配置上的AllowedScopes
配置有点不对
改用以下常量。
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
}
3 您还没有真正展示如何获取 id_tokens..
.. 但请记住向 idsrv 提供您想要 id_token 返回的请求,包括来自 profile
范围的声明。对 /authorize
、 /userinfo
或 /token
端点的请求必须换句话说包含一个 scope
参数,其值至少为 openid profile
.
例如,就像 Brock A. 在他的 oidc js 客户端库中所做的那样: https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/JavaScriptImplicitClient/app.js#L18