使用Swiss E-ID(swiyu)结合Duende和.NET Aspire实现用户身份验证

本文详细介绍了如何利用瑞士数字身份基础设施(swiyu)与Duende IdentityServer、ASP.NET Core Identity集成,实现基于可验证凭证的用户身份验证流程。内容涵盖注册与认证的具体实现、技术架构及安全考量。

使用swiyu(瑞士E-ID)通过Duende和.NET Aspire验证用户

本文展示了如何使用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(瑞士E-ID)通过Duende和.NET Aspire验证用户
  • 使用swiyu(瑞士E-ID)、Duende IdentityServer、ASP.NET Core Identity和.NET Aspire实现MFA
  • 使用swiyu、ASP.NET Core Identity和Aspire实现忘记密码功能

设置

使用Duende身份服务器作为Web应用程序的OpenID Connect服务器。当用户认证时,可以在身份提供程序内部使用瑞士E-ID来完成认证。应用程序使用.NET Aspire、ASP.NET Core Identity和瑞士公测版通用容器实现。这些容器实现了OpenID可验证凭证标准,并提供了简单的API来集成不同的应用程序。使用swiyu很简单,但并不是一种好的认证方式,因为它无法抵御网络钓鱼攻击。它是一种进行身份检查的好方法。

注册流程

要使用swiyu注册,使用以下四个声明来识别个人:

  • birth_date
  • birth_place
  • family_name
  • given_name

注册端点要求用户已认证。账户已经注册了电子邮件并创建了密码认证。然后可以添加E-ID,并将声明附加到该账户。

要使用瑞士E-ID注册并识别用户,需要使用瑞士通用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
42
43
44
45
46
47
48

{
    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;
}

UI开始轮询后端,以检查验证是否完成。该过程在带外完成,与会话没有连接。这是一个问题,因为无法防止网络钓鱼。发送的数据是正确的,但它与使用身份的会话是断开的。一旦用户使用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")
        {
            // 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.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();
            }
        }
         
        return Ok(verificationModel);
    }
    catch (Exception ex)
    {
        return BadRequest(new { error = "400", error_description = ex.Message });
    }
}

认证流程

用户拥有一个账户,可以使用瑞士E-ID进行认证。用户和应用程序使用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)
    {
        // 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();
}

使用了基于Duende默认模板适配的基础登录界面。

登录UI轮询并检查展示的状态。如果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
61
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);
                 

                await _signInManager.SignInWithClaimsAsync(user, null, claims);
                 
                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);
                    }
                }
                 
                return Redirect(ReturnUrl);
            }
        }
    }
    catch (Exception ex)
    {
        return BadRequest(new { error = "400", error_description = ex.Message });
    }
     
    return Page();
}

身份提供程序显示一个二维码来验证凭证。

一旦账户被验证,OpenID Connect流程即可完成。

注意事项

使用瑞士E-ID(Swiyu)进行身份识别是一个极好的解决方案,但它并不是认证的好方法。这是一种弱认证,可能被网络钓鱼。对于需要低级别认证的业务流程,将此视为可接受的风险是可以的。应始终可以使用更强的方式进行认证,例如使用通行密钥(passkeys)。

swiyu的使用场景:

  • 注册
  • 入职
  • 恢复
  • 升级验证
  • 身份检查

链接

相关标准

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