实现MFA:使用瑞士电子身份(swiyu)结合Duende IdentityServer、ASP.NET Core Identity和.NET Aspire
2025年11月10日 · 作者 damienbod · 分类: .NET Core, ASP.NET Core, e-id, OAuth2, OpenId, 安全, 自主身份
本文展示了如何在基于ASP.NET Core Identity和Duende IdentityServer的ASP.NET Core Web应用程序中,使用瑞士数字身份和信任基础设施(swiyu)作为多因素认证(MFA)方法。利用swiyu的通用容器来集成瑞士电子身份(E-ID)和可验证凭证的OpenID标准。
代码库: https://github.com/swiss-ssi-group/swiyu-idp-mfa-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实现“忘记密码”功能
配置
该Web应用程序配置为使用OpenID Connect进行认证。它使用一个带有授权码流和PKCE的机密客户端。OpenID Connect服务器通过Duende IdentityServer和ASP.NET Core Identity实现。为了进行E-ID验证,服务器集成了Swiyu通用容器以支持OpenID for Verifiable Presentations。使用Swiyu钱包来管理E-ID。
使用ASP.NET Core Identity集成
在此设置中,引入了自定义的多因素认证(MFA)提供程序。SwiyuUserTwoFactorTokenProvider类负责集成swiyu MFA。它实现了IUserTwoFactorTokenProvider接口,这是与ASP.NET Core Identity集成所必需的。此实现使用ApplicationUser类,并允许应用程序支持额外的MFA步骤,而不仅仅是密码用户认证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
using Idp.Swiyu.IdentityProvider.Models;
using Microsoft.AspNetCore.Identity;
namespace Idp.Swiyu.IdentityProvider.SwiyuServices;
public class SwiyuUserTwoFactorTokenProvider : IUserTwoFactorTokenProvider<ApplicationUser>
{
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
{
return Task.FromResult(true);
}
public Task<string> GenerateAsync(string purpose, UserManager<ApplicationUser> manager, ApplicationUser user)
{
return Task.FromResult(SwiyuConsts.SWIYU);
}
public Task<bool> ValidateAsync(string purpose, string token, UserManager<ApplicationUser> manager, ApplicationUser user)
{
return Task.FromResult(true);
}
}
|
该接口在ASP.NET Core Identity中使用。MFA提供程序通过AddTokenProvider方法添加。
1
2
3
4
|
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddTokenProvider<SwiyuUserTwoFactorTokenProvider>(SwiyuConsts.SWIYU)
.AddDefaultTokenProviders();
|
数据库设置
MFA提供程序需要数据持久化才能工作。为此使用了SwiyuIdentity类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<SwiyuIdentity> SwiyuIdentity => Set<SwiyuIdentity>();
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<SwiyuIdentity>().HasKey(m => m.Id);
builder.Entity<SwiyuIdentity>().Property(b => b.Id).ValueGeneratedOnAdd();
base.OnModelCreating(builder);
}
}
|
SwiyuIdentity类持久化从可验证凭证请求的数据。以下声明用于身份检查:
- name
- family_name
- birth_place
- birth_date
该实体通过UserId关联到用户。
1
2
3
4
5
6
7
8
9
10
11
12
|
public class SwiyuIdentity
{
public int Id { get; set; }
public string GivenName { get; set; } = null!;
public string FamilyName { get; set; } = null!;
public string BirthPlace { get; set; } = null!;
public string BirthDate { get; set; } = null!;
public string UserId { get; set; } = null!;
public string Email { get; set; } = null!;
}
|
注册swiyu作为MFA
设置完成后,已认证的用户可以使用RegisterSwiyuModel Razor页面将swiyu注册为MFA方法。这将创建一个可验证凭证展示,用户可以从其控制的swiyu钱包出示凭证。使用Javascript来处理来自钱包的响应。
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
|
using Idp.Swiyu.IdentityProvider.SwiyuServices;
using ImageMagick;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Net.Codecrete.QrCodeGenerator;
using System.Text.Json;
namespace Idp.Swiyu.IdentityProvider.Pages.Account.Manage;
[Authorize]
public class RegisterSwiyuModel : PageModel
{
private readonly VerificationService _verificationService;
private readonly string? _swiyuOid4vpUrl;
[BindProperty]
public string? VerificationId { get; set; }
[BindProperty]
public string? QrCodeUrl { get; set; } = string.Empty;
[BindProperty]
public byte[] QrCodePng { get; set; } = [];
public RegisterSwiyuModel(VerificationService verificationService,
IConfiguration configuration)
{
_verificationService = verificationService;
_swiyuOid4vpUrl = configuration["SwiyuOid4vpUrl"];
QrCodeUrl = QrCodeUrl.Replace("{OID4VP_URL}", _swiyuOid4vpUrl);
}
public void OnGet()
{
// 默认HTTP GET是必需的
}
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;
}
}
|
RegisterController webhook
RegisterController用于处理来自swiyu移动钱包的回调。该端点使用VerificationService来处理可验证凭证。如果响应正确,数据将持久化到数据库中。每个帐户只有一个凭证,正确实现时,需要使其更加健壮。
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
using Duende.IdentityModel;
using Idp.Swiyu.IdentityProvider.Data;
using Idp.Swiyu.IdentityProvider.Models;
using Idp.Swiyu.IdentityProvider.SwiyuServices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Net.Mail;
using System.Security.Claims;
namespace Swiyu.Aspire.Mgmt.Controllers;
[Route("api/[controller]")]
[ApiController]
public class RegisterController : ControllerBase
{
private readonly VerificationService _verificationService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _applicationDbContext;
public RegisterController(VerificationService verificationService,
UserManager<ApplicationUser> userManager,
ApplicationDbContext applicationDbContext)
{
_verificationService = verificationService;
_userManager = userManager;
_applicationDbContext = applicationDbContext;
}
[HttpGet("verification-response")]
public async Task<ActionResult> VerificationResponseAsync([FromQuery] string? id)
{
try
{
if (id == null)
{
return BadRequest(new { error = "400", error_description = "缺少参数 '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已被使用并关联到一个账户...");
}
if (user != null && user != null && (user.SwiyuIdentityId == 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();
await _userManager.SetTwoFactorEnabledAsync(user, true);
}
else
{
throw new Exception("swiyu,无法添加,未知错误...");
}
}
return Ok(verificationModel);
}
catch (Exception ex)
{
return BadRequest(new { error = "400", error_description = ex.Message });
}
}
public static string? GetEmail(IEnumerable<Claim> claims)
{
var email = claims.FirstOrDefault(t => t.Type == ClaimTypes.Email);
if (email != null)
{
return email.Value;
}
email = claims.FirstOrDefault(t => t.Type == JwtClaimTypes.Email);
if (email != null)
{
return email.Value;
}
email = claims.FirstOrDefault(t => t.Type == "preferred_username");
if (email != null)
{
var isNameAndEmail = IsEmailValid(email.Value);
if (isNameAndEmail)
{
return email.Value;
}
}
return null;
}
public static bool IsEmailValid(string email)
{
if (!MailAddress.TryCreate(email, out var mailAddress))
return false;
// 如果你想更严格一点:
var hostParts = mailAddress.Host.Split('.');
if (hostParts.Length == 1)
return false; // 没有点
if (hostParts.Any(p => p == string.Empty))
return false; // 双点
if (hostParts[^1].Length < 2)
return false; // TLD只有一个字母
if (mailAddress.User.Contains(' '))
return false;
if (mailAddress.User.Split('.').Any(p => p == string.Empty))
return false; // 用户部分双点或点结尾
return true;
}
}
|
禁用swiyu MFA
可以使用DisableSwiyuModel Razor页面禁用swiyu MFA。如果与其他MFA方法一起使用,则需要改进对现有MFA的检查。
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
using Idp.Swiyu.IdentityProvider.Data;
using Idp.Swiyu.IdentityProvider.Models;
using Idp.Swiyu.IdentityProvider.SwiyuServices;
using ImageMagick;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Shared;
using Net.Codecrete.QrCodeGenerator;
using System.Text.Json;
namespace Idp.Swiyu.IdentityProvider.Pages.Account.Manage;
[Authorize]
public class DisableSwiyuModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<DisableSwiyuModel> _logger;
private readonly ApplicationDbContext _applicationDbContext;
/// <summary>
/// 此API支持ASP.NET Core Identity默认UI基础结构,不打算从您的代码直接使用。
/// 此API可能在未来的版本中更改或移除。
/// </summary>
[TempData]
public string? StatusMessage { get; set; }
public DisableSwiyuModel(
UserManager<ApplicationUser> userManager,
ApplicationDbContext applicationDbContext,
ILogger<DisableSwiyuModel> logger)
{
_userManager = userManager;
_logger = logger;
_applicationDbContext = applicationDbContext;
}
public async Task<IActionResult> OnGet()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"无法加载ID为 '{_userManager.GetUserId(User)}' 的用户。");
}
if (!await _userManager.GetTwoFactorEnabledAsync(user))
{
throw new InvalidOperationException($"无法为用户禁用2FA,因为它当前未启用。");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"无法加载ID为 '{_userManager.GetUserId(User)}' 的用户。");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException($"禁用2FA时发生意外错误。");
}
var exists = _applicationDbContext.SwiyuIdentity.FirstOrDefault(c => c.UserId == user!.Id);
if (exists != null)
{
_applicationDbContext.SwiyuIdentity.Remove(exists);
await _applicationDbContext.SaveChangesAsync();
}
_logger.LogInformation("ID为 '{UserId}' 的用户已禁用2fa。", _userManager.GetUserId(User));
StatusMessage = "2fa已禁用。您可以在设置身份验证器应用时重新启用2fa";
return RedirectToPage("./TwoFactorAuthentication");
}
}
|
验证服务
VerificationService实现了创建可验证凭证展示以及处理webhook响应的逻辑。该服务根据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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
using System.Text;
using System.Text.Json;
using System.Web;
namespace Idp.Swiyu.IdentityProvider.SwiyuServices;
public class VerificationService
{
private readonly ILogger<VerificationService> _logger;
private readonly string? _swiyuVerifierMgmtUrl;
private readonly HttpClient _httpClient;
public VerificationService(IHttpClientFactory httpClientFactory,
ILoggerFactory loggerFactory, IConfiguration configuration)
{
_swiyuVerifierMgmtUrl = configuration["SwiyuVerifierMgmtUrl"];
_httpClient = httpClientFactory.CreateClient();
_logger = loggerFactory.CreateLogger<VerificationService>();
}
/// <summary>
/// curl -X POST http://localhost:8082/api/v1/verifications \
/// -H "accept: application/json" \
/// -H "Content-Type: application/json" \
/// -d '
/// </summary>
public async Task<string> CreateBetaIdVerificationPresentationAsync()
{
_logger.LogInformation("正在创建验证展示");
// 来自 "betaid-sdjwt"
var inputDescriptorsId = Guid.NewGuid().ToString();
var presentationDefinitionId = "00000000-0000-0000-0000-000000000000"; // Guid.NewGuid().ToString();
var json = GetBetaIdVerificationPresentationBody(inputDescriptorsId,
return await SendCreateVerificationPostRequest(json);
}
public async Task<VerificationManagementModel?> GetVerificationStatus(string verificationId)
{
var idEncoded = HttpUtility.UrlEncode(verificationId);
using HttpResponseMessage response = await _httpClient.GetAsync(
$"{_swiyuVerifierMgmtUrl}/api/v1/verifications/{idEncoded}");
if (response.IsSuccessStatusCode)
{
var jsonResponse = await response.Content.ReadAsStringAsync();
if (jsonResponse == null)
{
_logger.LogError("GetVerificationStatus: Swiyu未返回数据");
return null;
}
// state: PENDING, SUCCESS, FAILED
return JsonSerializer.Deserialize<VerificationManagementModel>(jsonResponse);
}
var error = await response.Content.ReadAsStringAsync();
_logger.LogError("无法创建验证展示 {vp}", error);
throw new ArgumentException(error);
}
/// <summary>
/// 在商业应用中,我们可以使用verificationModel中的数据
/// 验证数据:
/// 使用:wallet_response/credential_subject_data
///
/// birth_date, given_name, family_name, birth_place
///
/// </summary>
/// <param name="verificationManagementModel"></param>
/// <returns></returns>
public VerificationClaims GetVerifiedClaims(VerificationManagementModel verificationManagementModel)
{
var json = verificationManagementModel.wallet_response!.credential_subject_data!.ToString();
var jsonElement = JsonDocument.Parse(json!).RootElement;
var claims = new VerificationClaims
{
BirthDate = jsonElement.GetProperty("birth_date").ToString(),
BirthPlace = jsonElement.GetProperty("birth_place").ToString(),
FamilyName = jsonElement.GetProperty("family_name").ToString(),
GivenName = jsonElement.GetProperty("given_name").ToString()
};
return claims;
}
private async Task<string> SendCreateVerificationPostRequest(string json)
{
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
$"{_swiyuVerifierMgmtUrl}/api/v1/verifications", jsonContent);
if (response.IsSuccessStatusCode)
{
var jsonResponse = await response.Content.ReadAsStringAsync();
return jsonResponse;
}
var error = await response.Content.ReadAsStringAsync();
_logger.LogError("无法创建验证展示 {vp}", error);
throw new ArgumentException(error);
}
/// <summary>
/// 将会有私营公司需要进行身份识别程序(例如KYC或颁发另一个凭证之前),
/// 要求提供given_name、family_name、birth_date和birth_place。
///
/// { "path": [ "$.birth_date" ] },
/// { "path": ["$.given_name"] },
/// { "path": ["$.family_name"] },
/// { "path": ["$.birth_place"] },
/// </summary>
{
var json = $$"""
{
"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" ] }
]
}
}
]
}
}
""";
return json;
}
}
|
使用swiyu登录
用户可以使用默认的登录Razor页面进行认证。LoginSwiyuMfaModel Razor页面用于启动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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using Idp.Swiyu.IdentityProvider.Data;
using Idp.Swiyu.IdentityProvider.Models;
using Idp.Swiyu.IdentityProvider.SwiyuServices;
using ImageMagick;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Net.Codecrete.QrCodeGenerator;
using System.Security.Claims;
using System.Text.Json;
namespace Idp.Swiyu.IdentityProvider.Pages.Login;
[AllowAnonymous]
public class LoginSwiyuMfaModel : PageModel
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IIdentityServerInteractionService _interaction;
private readonly IEventService _events;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IIdentityProviderStore _identityProviderStore;
private readonly IHttpClientFactory _clientFactory;
private readonly ApplicationDbContext _applicationDbContext;
[BindProperty]
public string? ReturnUrl { get; set; }
private readonly VerificationService _verificationService;
private readonly string? _swiyuOid4vpUrl;
[BindProperty]
public string? VerificationId { get; set; }
[BindProperty]
public string? QrCodeUrl { get; set; } = string.Empty;
[BindProperty]
public byte[]? QrCodePng { get; set; } = [];
public LoginSwiyuMfaModel(
IIdentityServerInteractionService interaction,
IAuthenticationSchemeProvider schemeProvider,
IIdentityProviderStore identityProviderStore,
IEventService events,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
VerificationService verificationService,
IHttpClientFactory clientFactory,
IConfiguration configuration,
ApplicationDbContext applicationDbContext)
{
_userManager = userManager;
_signInManager = signInManager;
_interaction = interaction;
_schemeProvider = schemeProvider;
_identityProviderStore = identityProviderStore;
_events = events;
_clientFactory = clientFactory;
_applicationDbContext = applicationDbContext;
_verificationService = verificationService;
_swiyuOid4vpUrl = configuration["SwiyuOid4vpUrl"];
QrCodeUrl = QrCodeUrl.Replace("{OID4VP_URL}", _swiyuOid4vpUrl);
}
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();
}
public async Task<IActionResult> OnPost()
{
VerificationClaims verificationClaims = null;
try
{
if (VerificationId == null)
{
return BadRequest(new { error = "400", error_description = "缺少参数 '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);
if (user == null)
{
// 这应该返回一个不透露具体错误信息的用户消息。
throw new ArgumentNullException("认证出错");
}
var result = await _signInManager.TwoFactorSignInAsync(SwiyuConsts.SWIYU, string.Empty, false, false);
if (context != null)
{
if (context.IsNativeClient())
{
// 客户端是原生应用,因此这种返回响应方式的改变是为了给终端用户提供更好的用户体验。
return this.LoadingPage(ReturnUrl ?? "~/");
}
}
// 请求一个本地页面
if (Url.IsLocalUrl(ReturnUrl))
{
return Redirect(ReturnUrl);
}
else if (string.IsNullOrEmpty(ReturnUrl))
{
return Redirect("~/");
}
else
{
// 用户可能点击了恶意链接 - 应该记录日志
throw new ArgumentException("无效的返回URL");
}
}
}
}
catch (Exception ex)
{
return BadRequest(new { error = "400", error_description = ex.Message });
}
return Page();
}
internal async Task<VerificationManagementModel> RequestSwiyuClaimsAsync(int interval, string verificationId)
{
var client = _clientFactory.CreateClient();
while (true)
{
var verificationModel = await _verificationService.GetVerificationStatus(verificationId);
if (verificationModel != null && verificationModel.state == "SUCCESS")
{
return verificationModel;
}
else
{
await Task.Delay(interval * 1000);
}
}
}
}
|
登录
登录Razor页面检查用户是否需要MFA,并重定向到正确的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
if (result.RequiresTwoFactor)
{
var user = await _userManager.FindByEmailAsync(Input.Username!);
var exists = _applicationDbContext.SwiyuIdentity.FirstOrDefault(c => c.UserId == user!.Id);
if (exists != null)
{
return RedirectToPage("../LoginSwiyuMfa", new { Input.ReturnUrl, Input.RememberLogin });
}
return RedirectToPage("../LoginWith2fa", new { ReturnUrl = Input.ReturnUrl, Input.RememberLogin });
}
|
备注
这种实现方式相当标准,用户可以使用swiyu进行MFA。这不是一种防钓鱼的MFA,但提供了强大的身份验证。更好的方法是使用Passkey进行认证,并使用swiyu进行身份检查。
链接
标准