使用Swiyu瑞士电子身份实现多因素认证:集成Duende IdentityServer与ASP.NET Core Identity

本文详细介绍了如何在ASP.NET Core应用中集成瑞士数字身份认证系统swiyu作为多因素认证方法,涵盖自定义MFA提供程序、数据库配置、验证服务实现等核心技术内容。

使用Swiyu瑞士电子身份实现多因素认证

本文展示了如何在ASP.NET Core Web应用程序中使用ASP.NET Core Identity和Duende IdentityServer,将瑞士数字身份和信任基础设施(swiyu)作为多因素认证(MFA)方法。使用了swiyu的通用容器来集成瑞士电子身份和可验证凭证的OpenID标准。

代码库:https://github.com/swiss-ssi-group/swiyu-idp-mfa-aspire-aspnetcore

设置

Web应用程序配置为使用OpenID Connect进行身份验证。它使用带有授权代码流和PKCE的机密客户端。OpenID Connect服务器使用Duende IdentityServer和ASP.NET Core Identity实现。对于电子身份验证,服务器集成了Swiyu通用容器以支持可验证凭证的OpenID。Swiyu钱包用于管理电子身份。

使用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()
    {
        // default HTTP GET is required
    }
    
    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 = "Missing argument 'id'" });
            }
            
            var verificationModel = await _verificationService.GetVerificationStatus(id);
            
            if (verificationModel != null && verificationModel.state == "SUCCESS")
            {
                // In a business app we can use the data from the verificationModel
                // Verification data:
                // Use: 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 != 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);
                    
                    // Save to DB
                    user.SwiyuIdentityId = swiyuIdentity.Id;
                    
                    await _applicationDbContext.SaveChangesAsync();
                    
                    await _userManager.SetTwoFactorEnabledAsync(user, true);
                }
                else
                {
                    throw new Exception("swiyu, could not add, unknown error...");
                }
            }
            
            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;
        
        // And if you want to be more strict:
        var hostParts = mailAddress.Host.Split('.');
        if (hostParts.Length == 1)
            return false; // No dot.
        if (hostParts.Any(p => p == string.Empty))
            return false; // Double dot.
        if (hostParts[^1].Length < 2)
            return false; // TLD only one letter.
        
        if (mailAddress.User.Contains(' '))
            return false;
        if (mailAddress.User.Split('.').Any(p => p == string.Empty))
            return false; // Double dot or dot at end of user part.
        
        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>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </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($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }
        
        if (!await _userManager.GetTwoFactorEnabledAsync(user))
        {
            throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled.");
        }
        
        return Page();
    }
    
    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }
        
        var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
        if (!disable2faResult.Succeeded)
        {
            throw new InvalidOperationException($"Unexpected error occurred disabling 2FA.");
        }
        
        var exists = _applicationDbContext.SwiyuIdentity.FirstOrDefault(c => c.UserId == user!.Id);
        
        if (exists != null)
        {
            _applicationDbContext.SwiyuIdentity.Remove(exists);
            await _applicationDbContext.SaveChangesAsync();
        }
        
        _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
        StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
        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("Creating verification presentation");
        
        // from "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 no data returned from Swiyu");
                return null;
            }
            
            //  state: PENDING, SUCCESS, FAILED
            return JsonSerializer.Deserialize<VerificationManagementModel>(jsonResponse);
        }
        
        var error = await response.Content.ReadAsStringAsync();
        _logger.LogError("Could not create verification presentation {vp}", error);
        
        throw new ArgumentException(error);
    }
    
    /// <summary>
    /// In a business app we can use the data from the verificationModel
    /// Verification data:
    /// Use: 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("Could not create verification presentation {vp}", error);
        
        throw new ArgumentException(error);
    }
    
    /// <summary>
    /// There will be private companies having a need to do identification routines (e.g. KYC or before issuing another credential), 
    /// asking for given_name, family_name, birth_date and 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
195
196
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)
        {
            // check if we are in the context of an authorization request
            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 = "Missing argument 'VerificationId'" });
            }
            
            var verificationModel = await RequestSwiyuClaimsAsync(1, VerificationId);
            
            verificationClaims = _verificationService.GetVerifiedClaims(verificationModel);
            
            // check if we are in the context of an authorization request
            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)
                    {
                        // This should return a user message with no info what went wrong.
                        throw new ArgumentNullException("error in authentication");
                    }
                    
                    var result = await _signInManager.TwoFactorSignInAsync(SwiyuConsts.SWIYU, string.Empty, false, false);
                                            
                    if (context != null)
                    {
                        if (context.IsNativeClient())
                        {
                            // The client is native, so this change in how to
                            // return the response is for better UX for the end user.
                            return this.LoadingPage(ReturnUrl ?? "~/");
                        }
                    }
                    
                    // request for a local page
                    if (Url.IsLocalUrl(ReturnUrl))
                    {
                        return Redirect(ReturnUrl);
                    }
                    else if (string.IsNullOrEmpty(ReturnUrl))
                    {
                        return Redirect("~/");
                    }
                    else
                    {
                        // user might have clicked on a malicious link - should be logged
                        throw new ArgumentException("invalid return 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,但提供了强大的身份验证。更好的方法是使用通行密钥进行身份验证,并使用swyiu进行身份检查。

链接

标准

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计