Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
152976 stories
·
33 followers

Android Weekly Issue #710

1 Share
Articles & Tutorials
Sponsored
An Android developer's guide to React Native breaks down the mental shift, from XML & Activities to components, hooks and state management - especially if you’re coming from View-based or Jetpack Compose. Read the full guide.
alt
Tezov outlines a minimal Koin configuration approach tailored for Kotlin Multiplatform projects.
Sebastian Sellmair and Azat Abdullin describe stable Compose Hot Reload 1.0 enabling zero-configuration dynamic UI updates in Compose Multiplatform.
Sponsored
Code 10x faster. Tell Firebender to create full screens, ship features, or fix bugs - and watch it do the work for you. It's been battle tested by the best android teams at companies like Tinder, Adobe, and Instacart.
alt
Luca presents NavEntryScope to bridge Hilt’s scope gaps and manage shared dependencies per navigation entry.
Bruno Lannoo shows how delegation and sub-agents help structure scalable AI agent architectures in Kotlin.
Márton Braun explains the essential changes developers must apply to keep Android projects compatible with Android Gradle Plugin 9.
Suhyeon Kim showcases a declarative haptic-feedback library that makes implementing cross-platform tactile interactions in Compose Multiplatform easy.
Anshul Vyas breaks down how Kotlin’s in, out, and reified generics enable safer, more flexible type-safe code with examples.
Nav Singh showcases Material3 ListItem upgrades, adding segmented variants and integrated selection and click behaviors with expressive elevation, shape, and motion support.
Angélica Oliveira and Aline Ayres explain configuring Android Studio’s AI with MCP servers, alternate models, and prompt libraries to enhance development workflows.
Vasya Drobushkov clarifies cancellation and exception handling in Kotlin coroutines and suggests relying on safe return types over exceptions.
Victor Brandalise outlines practical techniques to minimize unnecessary recompositions in Jetpack Compose for better performance.
Segun Famisa shows how custom text rendering in Compose enables deeper control using TextMeasurer and Canvas.
Joe Birch demonstrates a responsive Compose TabRow that adapts layout based on available space.
Eevis Panula shows how Compose layouts can adapt content structure, not just text size, to improve accessibility.
Place a sponsored post
We reach out to more than 80k Android developers around the world, every week, through our email newsletter and social media channels. Advertise your Android development related service or product!
alt
Libraries & Code
Customizable Fling Physics for Jetpack Compose. Take full control of scroll momentum in LazyColumn, LazyRow, Pagers, and more with 9+ presets, snap behavior, and adaptive physics.
alt
Mozart is a library that allows you to create Android Live Wallpapers using Jetpack Compose.
Declarative Haptic Feedback Library for Compose Multiplatform
News
Elvira Mustafina introduces Compose Multiplatform 1.10.0 with performance, API, and stability improvements across all supported platforms.
Android Gradle plugin 9.0 is a major release that brings API and behavior changes.
Google introduces more flexible LLM selection and enhanced Agent Mode capabilities in Android Studio to support complex, multi-step developer workflows.
Videos & Podcasts
Discover the new features and updates in Android Studio Otter Feature Drops 1, 2, and 3.
alt
Join hosts Tor, Chet, and Romain as they sit down with Diego Perez (Android Studio) and Patrick Fuentes (Developer Relations) to explore the new frontiers of Android XR.
We know we're not supposed to use `GlobalScope`. Dave Leeds examines what CoroutineScope we should use instead.
Fragmented is changing. New direction, new cohost. Kaushik explains the pivot from Android to AI development and introduces Iury Souza.
Philipp Lackner talks about which navigation library in Jetpack Compose is recommended in a real production Android app in early 2026.
Angélica Oliveira and Aline Ayres show how to enhance Android Studio AI with MCP servers, external models, and custom prompt libraries.
Specials
Chet Haase tells a "fictional" podcast origin story to reflect on how creative projects change once reality meets initial enthusiasm.
Paul Samuels shares insights from evolving a developer tool to improve developer experience by reducing friction, enhancing usability, and fostering broader contribution
JetBrains is running a quick 3-minute survey for Android developers about cross-platform development, including Kotlin-based code sharing: what you’ve heard of, how you feel about different approaches, and what influences the decision to use them (or not). As a thank you, thoughtful responses can enter a raffle to win either a $50 Amazon gift card or a 6-month JetBrains All Products Pack subscription.
Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Just Because You Have an EA and Enterprise Support Does Not Mean You Are Ready for a Breach

1 Share
There is a common and risky assumption in Microsoft Security Support belief I hear more often than I probably should, especially in large enterprise environments where a customer may have an Enterprise Agreement (EA): We have an EA and Enterprise Support. Microsoft will take care of us if something bad happens. I understand where that belief comes from. Enterprise Agreement sounds comprehensive...thorough, even. Enterprise Support sounds like you are covered no matter what. On paper, it feels...

Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Implementing Level of Identification (LoI) with ASP.NET Core Identity and Duende

1 Share

This article explores how to implement Level of Identification (LOI) in an ASP.NET Core application. The solution uses Duende IdentityServer as the OpenID Connect provider and ASP.NET Core Identity for user management. Identity verification is performed using the Swiyu Public Beta infrastructure.

Any OpenID Connect client can consume the loi claim together with the loa claim to enforce security‑related business requirements.

Code: https://github.com/swiss-ssi-group/swiyu-passkeys-idp-loi-loa

Blogs in this series:

Setup

The solution consists of multiple containers and databases. One of these containers, the swiyu verifier, provides the Swiyu Public Beta infrastructure. This setup enables the use of the Swiss E‑ID to verify credentials and the corresponding identity. The identity provider integrates with this container, which exposes a management API to initiate verifications and query their results. The verifier implements the OpenID4VP standard, which is used by the Wallet during verifiable presentations.

ASP.NET Core Identity is used to implement the IAM capabilities. Passkeys are supported, and the identification checks are seamlessly integrated into the existing UI. Duende IdentityServer is responsible for implementing the OpenID Connect and OAuth standards. Users can create an account and bind their Swiss E‑ID to it, including the required profile attributes. These attributes cannot be updated manually afterward and can only be refreshed via Swiyu.

The web application and any other relying applications do not interact with Swiyu directly. Instead, they authenticate using the OpenID Connect server. The server returns the Level of Identification (LOI) and Level of Authentication (LOA) claims, along with the amr claim describing the authentication method used.

Microsoft Aspire is used to orchestrate and manage the containerized setup. All containers are configured within the Idp.Swiyu.Passkeys.AppHost project. Based on the Swiyu Public Beta documentation, the required configuration values and secrets are loaded into the respective projects, ensuring each component has the correct environment settings for verification, identity management, and OpenID Connect operations.

/////////////////////////////////////////////////////////////////
// Verifier OpenID Endpoint: Must be deployed to a public URL
/////////////////////////////////////////////////////////////////
// Verifier Management Endpoint: TODO Add JWT security verifier
// Add security to management API, disabled
// https://github.com/swiyu-admin-ch/swiyu-verifier?tab=readme-ov-file#security
/////////////////////////////////////////////////////////////////
swiyuVerifier = builder.AddContainer("swiyu-verifier", "ghcr.io/swiyu-admin-ch/swiyu-verifier", "latest")
    .WithEnvironment("EXTERNAL_URL", verifierExternalUrl)
    .WithEnvironment("OPENID_CLIENT_METADATA_FILE", verifierOpenIdClientMetaDataFile)
    .WithEnvironment("VERIFIER_DID", verifierDid)
    .WithEnvironment("DID_VERIFICATION_METHOD", didVerifierMethod)
    .WithEnvironment("SIGNING_KEY", verifierSigningKey)
    .WithEnvironment("POSTGRES_USER", postGresUser)
    .WithEnvironment("POSTGRES_PASSWORD", postGresPassword)
    .WithEnvironment("POSTGRES_DB", postGresDbVerifier)
    .WithEnvironment("POSTGRES_JDBC", postGresJdbcVerifier)
    .WithHttpEndpoint(port: 8084, targetPort: 8080, name: HTTP)  // local development
    //.WithHttpEndpoint(port: 80, targetPort: 8080, name: HTTP) // for deployment 
    .WithExternalHttpEndpoints();

identityProvider = builder.AddProject<Projects.Idp_Swiyu_Passkeys_Sts>(IDENTITY_PROVIDER)
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache)
    .WithEnvironment("SwiyuVerifierMgmtUrl", swiyuVerifier.GetEndpoint(HTTP))
    .WithEnvironment("SwiyuOid4vpUrl", verifierExternalUrl)
    .WithEnvironment("ISSUER_ID", issuerId)
    .WaitFor(swiyuVerifier);

The verification application requires a publicly reachable endpoint to function during development. To support this, I deployed the Swiyu Verifier container first and connected it to a public database. The development deployment uses the same setup. When running locally, the local Swiyu Verifier container is used to create the presentation, and the deployed verifier shares the same database which communicates with the Wallet through its public endpoint.

Some configuration files are retrieved from a public server.

As an alternative, the entire setup can be run locally while exposing a public endpoint using ngrok or the Visual Studio Dev Tunnels feature.

Since the Swiyu Verifier container acts as a public endpoint, its API MUST be secured. This can be achieved by placing the verifier behind a secure reverse proxy, implementing OAuth-based protection, or at minimum requiring a signed JWT. Ideally, multiple layers of security should be applied. The generic containers should also support DPoP access tokens, which would further strengthen the security model.

loi (Level of Identification)

Every user that authenticates using the identity provider has a level of identification. In this demo, only level loi.100 and loi.400 are supported. When the user has authenticated and completed a verification with swiyu, the account and the returned identity has a loi.400 (OpenID verifiable credentials (E-ID, swiyu), government issued). Otherwise the user only has a level 100, even when authenticating using passkeys.

  • loi.500 : Offline Human identification by trusted official in trustworthy organisation.
  • loi.400 : OpenID verifiable credentials (E-ID, swiyu), government issued.
  • loi.300 : Digital online check with person
  • loi.200 : Digital video without person
  • loi.100 : Email & SMS validation

When an end user would like to authenticate in a web application, the person can do it in different ways. Note: The account has already been verified with the swiyu in all the different variants.

Variant 1:
–      User starts OpenID Connect code flow for web application
–      On the IDP, user authenticates using a Wallet and OpenID4VP directly.
–      Id_token, or user profile data returns with the amr claim

amr: mca | loi: loi.400 | loa: loa.200

Note: The loa is 200 as this is single factor authentication and easy to phish.

Variant 2:
–      User starts OpenID Connect code flow for web application
–      On the IDP, user authenticates first factor using password
–      On the IDP, user authenticates second factor using a Wallet and OpenID4VP

amr: mfa | loi: loi.400 | loa: loa.300

Variant 3:
–      User starts OpenID Connect code flow for web application
–      On the IDP, user authenticates using passkeys
–      On the IDP, user authenticates second factor using a Wallet and OpenID4VP

amr: pop | loi: loi.400 | loa: loa.400

Variant 4: (Account identification is completed and data updated.)
–      User starts OpenID Connect code flow for web application
–      On the IDP, user authenticates using passkeys

amr: pop | loi: loi.400 | loa: loa.400

Implement swiyu Public Beta Infrastructure in the IDP

The identification process is implemented using the Public Beta cookbooks and the generic containers. After setting up everything, the required APIs and configurations can be used in the solution through the swiyu verifier container.

This container implements the management APIs to create presentations and also the OpenID4VP standard to interact with the wallet. The API used by the wallet must be public, even when developing.

Following the Public Beta documentation, the VerificationService implements the client to interact with the management APIs support in the verifier container. The presentation uses four claims to verify the identity: birth_date, given_name, family_name, birth_place. If more are required, then extend the presentation request as requires.

using System.Text;
using System.Text.Json;
using System.Web;

namespace Idp.Swiyu.Passkeys.Sts.SwiyuServices;

public class VerificationService
{
    private readonly ILogger<VerificationService> _logger;
    private readonly string? _swiyuVerifierMgmtUrl;
    private readonly string? _issuerId;
    private readonly HttpClient _httpClient;

    public VerificationService(IHttpClientFactory httpClientFactory,
        ILoggerFactory loggerFactory, IConfiguration configuration)
    {
        _swiyuVerifierMgmtUrl = configuration["SwiyuVerifierMgmtUrl"];
        _issuerId = configuration["ISSUER_ID"];
        _httpClient = httpClientFactory.CreateClient();
        _logger = loggerFactory.CreateLogger<VerificationService>();
    }

    /// <summary>
    /// curl - X POST http://localhost:8082/management/api/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 acceptedIssuerDid = "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527";

        var inputDescriptorsId = Guid.NewGuid().ToString();
        var presentationDefinitionId = "00000000-0000-0000-0000-000000000000"; // Guid.NewGuid().ToString();

        var json = GetBetaIdVerificationPresentationBody(inputDescriptorsId,
            presentationDefinitionId, acceptedIssuerDid, "betaid-sdjwt");

        return await SendCreateVerificationPostRequest(json);
    }


    public async Task<VerificationManagementModel?> GetVerificationStatus(string verificationId)
    {
        var idEncoded = HttpUtility.UrlEncode(verificationId);
        using HttpResponseMessage response = await _httpClient.GetAsync(
            $"{_swiyuVerifierMgmtUrl}/management/api/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}/management/api/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>
    private static string GetBetaIdVerificationPresentationBody(string inputDescriptorsId, string presentationDefinitionId, string acceptedIssuerDid, string vcType)
    {
        var json = $$"""
             {
                 "accepted_issuer_dids": [ "{{acceptedIssuerDid}}" ],
                 "response_mode": "direct_post",
                 "presentation_definition": {
                     "id": "{{presentationDefinitionId}}",
                     "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;
    }
}

The RegisterModel Razor Page is used to attach an swiyu identification to an existing account. The user MUST be authenticated to do this. I normally require at least MFA before allowing the process. Passkeys authentication would be perfect here. When the verification is successfully completed, the ConnectAccountWithIdentity method is used to update the claims and save the authentic data. From this point onwards, this data cannot be updated manually and the used has a level of identification of 400.

namespace Idp.Swiyu.Passkeys.Sts.Pages.Swiyu;

[Authorize]
public class RegisterModel : PageModel
{
    private readonly ApplicationDbContext _applicationDbContext;
    private readonly IHttpClientFactory _clientFactory;
    private readonly VerificationService _verificationService;
    private readonly string? _swiyuOid4vpUrl;
    private readonly UserManager<ApplicationUser> _userManager;

    [BindProperty]
    public string? VerificationId { get; set; }

    [BindProperty]
    public string? QrCodeUrl { get; set; } = string.Empty;

    [BindProperty]
    public byte[] QrCodePng { get; set; } = [];

    public RegisterModel(VerificationService verificationService,
        IConfiguration configuration,
        IHttpClientFactory clientFactory,
        ApplicationDbContext applicationDbContext,
        UserManager<ApplicationUser> userManager)
    {
        _applicationDbContext = applicationDbContext;
        _clientFactory = clientFactory;
        _verificationService = verificationService;
        _swiyuOid4vpUrl = configuration["SwiyuOid4vpUrl"];
        QrCodeUrl = QrCodeUrl.Replace("{OID4VP_URL}", _swiyuOid4vpUrl);
        _userManager = userManager;
    }

    public async Task OnGetAsync()
    {
        var user = await _userManager.FindByEmailAsync(GetEmail(User.Claims)!);
        var swiyuVerifiedIdentity = _applicationDbContext.SwiyuIdentity.FirstOrDefault(si => si.UserId == user!.Id);

        if(swiyuVerifiedIdentity != null)
        {
            // User already has a verified Swiyu identity, redirect to complete page
            Response.Redirect("/Swiyu/IdentityAlreadyVerified");
            return;
        }
   
        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;
    }

    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 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");
                }
            }
            else
            {
                await ConnectAccountWithIdentity(verificationClaims);
                return Redirect("/Swiyu/IdentityCheckComplete");
            }

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

    private async Task ConnectAccountWithIdentity(VerificationClaims verificationClaims)
    {
        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 == 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();

            // remove demo claims
            await _userManager.RemoveClaimsAsync(user, await _userManager.GetClaimsAsync(user));
        }     
    }

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

loi 100

If the user authenticates using a password and the account has not been verified, the loi is returned with a value of 100. Even if the user authenticates using passkeys, 100 is still returned for the loi.

  private static List<Claim> GetAdditionalClaims(SwiyuIdentity? swiyuVerifiedIdentity, string loaValue, string amr)
  {
      List<Claim> additionalClaims;
      if (swiyuVerifiedIdentity != null)
      {
          additionalClaims = new List<Claim>
          {
              new Claim(Consts.LOA, loaValue),
              new Claim(Consts.LOI, Consts.LOI_400),
              // ASP.NET Core bug workaround:
              // https://github.com/dotnet/aspnetcore/issues/64881
              new Claim(JwtClaimTypes.AuthenticationMethod, amr),

              new Claim(JwtClaimTypes.GivenName, swiyuVerifiedIdentity.GivenName),
              new Claim(JwtClaimTypes.FamilyName, swiyuVerifiedIdentity.FamilyName),
              new Claim(JwtClaimTypes.BirthDate, swiyuVerifiedIdentity.BirthDate),
              new Claim("birth_place", swiyuVerifiedIdentity.BirthPlace)
          };
      }
      else
      {
          additionalClaims = new List<Claim>
          {
              new Claim(Consts.LOA, loaValue),
              new Claim(Consts.LOI, Consts.LOI_100),
              // ASP.NET Core bug workaround:
              // https://github.com/dotnet/aspnetcore/issues/64881
              new Claim(JwtClaimTypes.AuthenticationMethod, amr)
          };
      }

      return additionalClaims;
  }

The values can be implemented as follows:

// Create additional claims
var additionalClaims = GetAdditionalClaims(swiyuVerifiedIdentity, Consts.LOA_100, Amr.Pwd);

// Sign in again with the additional claims
await _signInManager.SignInWithClaimsAsync(user!, isPersistent: false, additionalClaims);

loi 400

Once the account is verified, loi is returned with a value of 400. The user can authenticate in different ways; password, Wallet, MFA or passkeys. Trusting the loi when weak authentication is used is dangerous. If using the loi, MFA should be required to authenticate when a proximity is not guaranteed.

[AllowAnonymous]
public class LoginModel : 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; } = default!;

    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 LoginModel(
        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 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 additionalClaims = GetAdditionalClaims(exists);
                    // issue authentication cookie for user
                    await _signInManager.SignInWithClaimsAsync(user, null, additionalClaims);

                    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();
    }

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

    private static List<Claim> GetAdditionalClaims(SwiyuIdentity swiyuVerifiedIdentity)
    {
        var additionalClaims = new List<Claim>
        {
            new Claim(Consts.LOA, Consts.LOA_300),
            new Claim(Consts.LOI, Consts.LOI_400),
            // ASP.NET Core bug workaround:
            // https://github.com/dotnet/aspnetcore/issues/64881
            new Claim(JwtClaimTypes.AuthenticationMethod, Amr.Mfa),

            new Claim(JwtClaimTypes.GivenName, swiyuVerifiedIdentity.GivenName),
            new Claim(JwtClaimTypes.FamilyName, swiyuVerifiedIdentity.FamilyName),
            new Claim(JwtClaimTypes.BirthDate, swiyuVerifiedIdentity.BirthDate),
            new Claim("birth_place", swiyuVerifiedIdentity.BirthPlace)
        };

        return additionalClaims;
    }

}

Example using password authentication and an identified account:

Authentication using passkeys with E-ID Identity verified

The recommended way to authenticate in an application is to use passkeys. This is one of the few phishing resistant authentication methods. Using OpenID4VP is NOT strong authentication, it is strong identification.

 result = await _signInManager.PasskeySignInAsync(Input.Passkey.CredentialJson);
 if (result.Succeeded)
 {
     user = await _userManager.GetUserAsync(User);
     var swiyuVerifiedIdentity = _applicationDbContext.SwiyuIdentity.FirstOrDefault(si => si.UserId == user!.Id);

     // Sign out first to clear the existing cookie
     await _signInManager.SignOutAsync();
     var additionalClaims = GetAdditionalClaims(swiyuVerifiedIdentity, Consts.LOA_400, Amr.Pop);

     // Sign in again with the additional claims
     await _signInManager.SignInWithClaimsAsync(user!, isPersistent: false, additionalClaims);
 }

when the account has completed an identity check and authenticates using passkeys, the best security is used.

In the next post, an API using OAuth DPoP access tokens, will implement authorization using loi.400 and loa.400.

Links

https://github.com/dotnet/aspnetcore/issues/64881

https://openid.net/specs/openid-connect-eap-acr-values-1_0-final.html

https://datatracker.ietf.org/doc/html/rfc8176

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims

SSI

https://www.eid.admin.ch/en/public-beta-e

https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview

https://www.npmjs.com/package/ngrok

https://swiyu-admin-ch.github.io/specifications/interoperability-profile/

https://andrewlock.net/converting-a-docker-compose-file-to-aspire/

https://swiyu-admin-ch.github.io/cookbooks/onboarding-generic-verifier/

https://github.com/orgs/swiyu-admin-ch/projects/2/views/2

SSI Standards

https://identity.foundation/trustdidweb/

https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html

https://openid.net/specs/openid-4-verifiable-presentations-1_0.html

https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/

https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/

https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/

https://www.w3.org/TR/vc-data-model-2.0/



Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 10: Zip and GZip API Improvements

1 Share
Introduction Compression APIs like ZipArchive and GZipStream have been part of .NET for years. They’ve...
Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

.NET 10: Post-Quantum Cryptography Comes to .NET

1 Share
Introduction Quantum computing is no longer just a research topic. As the technology matures, the...
Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

AI for human agency

1 Share
How AI can expand human agency by closing the capability overhang—helping people, businesses, and countries unlock real productivity, growth, and opportunity.
Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories