本文展示了如何使用Duende IdentityServer和ASP.NET Core Identity对用户进行身份验证,该验证过程通过瑞士数字身份和信任基础设施(swiyu)来验证身份(即可验证的数字凭证)。swiyu基础设施使用提供的通用容器实现,这些容器实现了OpenID for Verifiable Presentations标准以及许多其他用于实现可验证凭证的标准。此基础设施可用于实现身份用例或基本的身份验证流程。
使用可验证凭证是实现身份识别用例的好方法,但它不是实现身份验证流程的好方法。当需要高安全性时,应将用户识别与强身份验证流程结合使用,例如,使用通行密钥(passkeys)进行身份验证,并使用可验证凭证进行身份识别。
代码库:https://github.com/swiss-ssi-group/swiyu-idp-aspire-aspnetcore
本系列博客
- 使用瑞士数字身份公测版、ASP.NET Core和.NET Aspire签发和验证凭证
- 使用swiyu(瑞士电子身份证)通过Duende和.NET Aspire对用户进行身份验证
- 使用swiyu(瑞士电子身份证)、Duende IdentityServer、ASP.NET Core Identity和.NET Aspire实现多因素身份验证(MFA)
- 使用swiyu、ASP.NET Core Identity和Aspire实现“忘记密码”功能
设置
使用Duende身份服务器作为Web应用程序的OpenID Connect服务器。当用户进行身份验证时,可以在身份提供程序中使用瑞士电子身份证来完成验证。应用程序使用.NET Aspire、ASP.NET Core Identity和瑞士公测版通用容器实现。这些容器实现了OpenID可验证凭证标准,并提供了简单的API来集成不同的应用程序。使用swiyu很简单,但它不是一个好的身份验证方式,因为它无法抵抗网络钓鱼攻击。它是进行身份检查的好方法。
注册流程
要使用swiyu进行注册,使用以下四个声明来识别个人:
- birth_date
- birth_place
- family_name
- given_name
注册端点要求用户已通过身份验证。账户需要已注册邮箱并创建了密码认证。然后可以添加电子身份证,并将声明附加到此账户。
要使用瑞士电子身份证注册并识别用户,需要使用瑞士通用API创建验证演示请求。OnPostAsync方法启动此过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public async Task OnPostAsync()
{
var presentation = await _verificationService
.CreateBetaIdVerificationPresentationAsync();
var verificationResponse = JsonSerializer.Deserialize<CreateVerificationPresentationModel>(presentation);
// verification_url
QrCodeUrl = verificationResponse!.verification_url;
var qrCode = QrCode.EncodeText(verificationResponse!.verification_url, QrCode.Ecc.Quartile);
QrCodePng = qrCode.ToPng(20, 4, MagickColors.Black, MagickColors.White);
VerificationId = verificationResponse.id;
}
|
请求体发送通用API中定义的所需演示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
{
"jwt_secured_authorization_request": true,
"presentation_definition": {
"id": "{{presentationDefinitionId}}",
"name": "Verification",
"purpose": "Verify using Beta ID",
"input_descriptors": [
{
"id": "{{inputDescriptorsId}}",
"format": {
"vc+sd-jwt": {
"sd-jwt_alg_values": [
"ES256"
],
"kb-jwt_alg_values": [
"ES256"
]
}
},
"constraints": {
"fields": [
{
"path": [
"$.vct"
],
"filter": {
"type": "string",
"const": "{{vcType}}"
}
},
{ "path": [ "$.birth_date" ] },
{ "path": [ "$.given_name" ] },
{ "path": [ "$.family_name" ] },
{ "path": [ "$.birth_place" ] }
]
}
}
]
}
}
|
用户界面开始轮询后端,以检查验证是否完成。该过程在带外完成,与会话没有连接。这是一个问题,因为无法防止网络钓鱼。发送的数据是正确的,但它与使用身份的会话是断开的。一旦用户使用swiyu钱包完成身份检查,用户信息将被更新并附加到账户。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
[HttpGet("verification-response")]
public async Task<ActionResult> VerificationResponseAsync([FromQuery] string? id)
{
try
{
if (id == null)
{
return BadRequest(new { error = "400", error_description = "Missing argument 'id'" });
}
var verificationModel = await _verificationService.GetVerificationStatus(id);
if (verificationModel != null && verificationModel.state == "SUCCESS")
{
// 在业务应用中,我们可以使用来自 verificationModel 的数据
// 验证数据:
// 使用:wallet_response/credential_subject_data
var verificationClaims = _verificationService.GetVerifiedClaims(verificationModel);
var user = await _userManager.FindByEmailAsync(GetEmail(User.Claims)!);
var exists = _applicationDbContext.SwiyuIdentity.FirstOrDefault(c =>
c.BirthDate == verificationClaims.BirthDate &&
c.BirthPlace == verificationClaims.BirthPlace &&
c.GivenName == verificationClaims.GivenName &&
c.FamilyName == verificationClaims.FamilyName);
if(exists != null)
{
throw new Exception("Swiyu already in use and connected to an account...");
}
if (user != null && user.SwiyuIdentityId <= 0)
{
var swiyuIdentity = new SwiyuIdentity
{
UserId = user.Id,
BirthDate = verificationClaims.BirthDate,
FamilyName = verificationClaims.FamilyName,
BirthPlace = verificationClaims.BirthPlace,
GivenName = verificationClaims.GivenName,
Email = user.Email!
};
_applicationDbContext.SwiyuIdentity.Add(swiyuIdentity);
// 保存到数据库
user.SwiyuIdentityId = swiyuIdentity.Id;
await _applicationDbContext.SaveChangesAsync();
}
}
return Ok(verificationModel);
}
catch (Exception ex)
{
return BadRequest(new { error = "400", error_description = ex.Message });
}
}
|
身份验证流程
用户拥有一个账户,可以使用瑞士电子身份证进行身份验证。用户和应用程序使用OpenID Connect从Web应用程序进行身份验证,并在身份提供程序上使用存储在手机上的凭证的瑞士钱包进行身份验证。任何可以访问手机的人都可以使用这些凭证。这也是安全方面的一个漏洞。
当身份提供程序打开后,使用登录端点。可以启动swiyu身份检查,它会像注册流程一样发送验证演示请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public async Task<IActionResult> OnGet(string? returnUrl)
{
if (returnUrl != null)
{
// 检查我们是否在授权请求的上下文中
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
ReturnUrl = returnUrl;
}
var presentation = await _verificationService
.CreateBetaIdVerificationPresentationAsync();
var verificationResponse = JsonSerializer.Deserialize<CreateVerificationPresentationModel>(presentation);
// verification_url
QrCodeUrl = verificationResponse!.verification_url;
var qrCode = QrCode.EncodeText(verificationResponse!.verification_url, QrCode.Ecc.Quartile);
QrCodePng = qrCode.ToPng(20, 4, MagickColors.Black, MagickColors.White);
VerificationId = verificationResponse.id;
return Page();
}
|
使用Duende默认模板的基本登录界面已做适配。
登录用户界面轮询并检查演示的状态。如果VerificationId有成功的响应,则使用声明并对身份进行身份验证。此处应添加一些额外的验证。最好也验证电子邮件。如果用户的电子邮件与验证数据附加的账户的电子邮件相同,则安全性会稍强一些。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
public async Task<IActionResult> OnPost()
{
VerificationClaims verificationClaims = null;
try
{
if (VerificationId == null)
{
return BadRequest(new { error = "400", error_description = "Missing argument 'VerificationId'" });
}
var verificationModel = await RequestSwiyuClaimsAsync(1, VerificationId);
verificationClaims = _verificationService.GetVerifiedClaims(verificationModel);
// 检查我们是否在授权请求的上下文中
var context = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (ModelState.IsValid)
{
var claims = new List<Claim>
{
new Claim("name", verificationClaims.GivenName),
new Claim("family_name", verificationClaims.FamilyName),
new Claim("birth_place", verificationClaims.BirthPlace),
new Claim("birth_date", verificationClaims.BirthDate)
};
var exists = _applicationDbContext.SwiyuIdentity.FirstOrDefault(c =>
c.BirthDate == verificationClaims.BirthDate &&
c.BirthPlace == verificationClaims.BirthPlace &&
c.GivenName == verificationClaims.GivenName &&
c.FamilyName == verificationClaims.FamilyName);
if (exists != null)
{
var user = await _userManager.FindByIdAsync(exists.UserId);
// 为用户颁发身份验证cookie
await _signInManager.SignInWithClaimsAsync(user, null, claims);
if (context != null)
{
if (context.IsNativeClient())
{
// 客户端是本地的,因此这种返回响应的方式是为了给最终用户带来更好的用户体验。
return this.LoadingPage(ReturnUrl);
}
}
return Redirect(ReturnUrl);
}
}
}
catch (Exception ex)
{
return BadRequest(new { error = "400", error_description = ex.Message });
}
return Page();
}
|
身份提供程序显示一个二维码来验证凭证。
账户验证后,OpenID Connect流程即可完成。
注意事项
使用瑞士电子身份证(Swiyu)进行身份识别是一个极好的解决方案,但它不是一个好的身份验证解决方案。这是一种弱身份验证,可能会受到网络钓鱼攻击。对于需要低级别身份验证的业务流程,可以接受此风险。应该始终可以使用更强的方式进行身份验证,例如使用通行密钥(passkeys)。
Swiyu的适用场景
相关链接
相关标准