skip to Main Content

I am trying to publish a Blazor WASM app to a Windows App Service server on Azure, but when I try to publish the app I am encountering problems with my Azure Key Vault that are preventing it from publishing.

When I first set up my publish settings inside Visual Studio it walked me through connecting to my various Service Dependencies. I was able to connect to them all fine as you can see here:

enter image description here

When I was creating the connection to the Azure Key Vault though, I noticed that part of the process added 2 lines of code to my program.cs file. These are the 2 lines it added:

var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
builder.Configuration.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());

This makes my app connect to my Azure Key vault on startup, and then from that point onwards it gets my secrets from the key vault rather than my appsettings.json files. However, this is where my problems begin.

The first issue is that when I run my app in debug mode it will connect to the key vault just fine, but when I run the Publish tool the line ‘Environment.GetEnvironmentVariable("VaultUri")’ fails and returns NULL and so my app crashes and won’t publish.

Can anyone tell me the reason why this line works correctly when I run the app in my visual studio debugger but it fails when publishing? Its as if the publisher completely ignores the Service Dependencies that it just helped me set up. Why?

The next problem is that I do not want my app to be fetching the secrets from my production key vault all the time even when I am debugging. I want to be using my local secrets and appsettings when I am debugging, not the production ones. So I simply added a ‘if (builder.Environment.IsDevelopment())’ and changed things so that it only connects to (and fetches secrets from) my key vault when its in production.

This works perfectly fine for local development and debugging. But now when I try to publish my app there is a line within the Entity Framework Migrations setting as follows that is causing it to fail again when publishing:

enter image description here

It is trying to get the database connection string from the key vault (as it correctly should) but of course it cant to because:

  1. I had to change it so that it doesn’t run the code to connect to the key vault when its in development mode, and
  2. Even when it does run the key vault connect code it fails anyway and returns null

So what am I supposed to do about this catch 22 situation? Ideally I need to make the publisher run as if it’s in Production mode and have it connect to the key vault, but I dont know if this is the correct way of doing things or if its even possible. Can anyone help?

As requested here is my appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "IdentityServer": {
    "Clients": {
      "SiteBuddy.Client": {
        "Profile": "IdentityServerSPA"
      }
    },
    "Key": {
      "Type": "File",
      "StoreName": "My",
      "FilePath": "blazor_wasm_cert.pfx"
    }
  },
  "AllowedHosts": "*"
}

My appsettings.Development.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=MY-PC;Database=MyDatabase;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

and my program.cs

global using Microsoft.EntityFrameworkCore;
global using MySiteName.Shared;
global using MySiteName.Server.Services.CompanyService;
global using MySiteName.Server.Services.UserService;
global using MySiteName.Server.Services.JobService;
global using MySiteName.Server.Services.FloorService;
global using MySiteName.Server.Services.BuildingService;
global using MySiteName.Server.Services.PlanService;
global using MySiteName.Server.Services.RoomService;
global using MySiteName.Server.Utilities;
global using MySiteName.Server.Services.StatusService;
global using MySiteName.Server.Services.IssueService;
global using MySiteName.Server.Services.ReportService;
global using MySiteName.Server.Services.NoteService;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.ResponseCompression;
using MySiteName.Server.Data;
using MySiteName.Server.Models;
using MySiteName.Server.Services.EmailSender;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Extensions.Azure;
using Azure.Identity;
using Microsoft.Extensions.Logging.AzureAppServices;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddAzureWebAppDiagnostics();
builder.Services.Configure<AzureFileLoggerOptions>(options =>
{
    options.FileName = "Logs-";
    options.FileSizeLimit = 50 * 1024;
    options.RetainedFileCountLimit = 5;
});

if (builder.Environment.IsDevelopment())
{
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

    builder.Services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseSqlServer(connectionString);
        // Dont allow the following line on prod
        options.EnableSensitiveDataLogging();
    });
}
else
{
    // If this gives a null excepted then make sure that you are connected to the Azure Key Vault
    // in Connected Services (just beneath MySiteName.Server in the solution explorer)
    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
    builder.Configuration.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());

    // Add services to the container.
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

    builder.Services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseSqlServer(connectionString);
    });
}
    
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();

var identityServer = builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(opt =>
    {   // This code gets the user Name to show up inside the this.User member variable of controllers
        opt.IdentityResources["openid"].UserClaims.Add("name");
        opt.ApiResources.Single().UserClaims.Add("name");
        // This code makes the roles work https://code-maze.com/using-roles-in-blazor-webassembly-hosted-applications/
        opt.IdentityResources["openid"].UserClaims.Add("role");
        opt.ApiResources.Single().UserClaims.Add("role");
    });

// This must also be done in order for roles to work for some reason
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var MyAllowedSpecificOrigins = "AzureBlobStorageOrigin";
// Add CORS policy to allow access to Azure Blobs storage
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowedSpecificOrigins,
        builder =>
        {
            builder
                .SetIsOriginAllowed((_) => true)
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });
});


// Add email sender
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);
// Add services here
builder.Services.AddScoped<ICompanyService, CompanyService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IJobService, JobService>();
builder.Services.AddScoped<IFloorService, FloorService>();
builder.Services.AddScoped<IBuildingService, BuildingService>();
builder.Services.AddScoped<IPlanService, PlanService>();
builder.Services.AddScoped<IRoomService, RoomService>();
builder.Services.AddScoped<IStatusService, StatusService>();
builder.Services.AddScoped<IIssueService, IssueService>();
builder.Services.AddScoped<IReportService, ReportService>();
builder.Services.AddScoped<INoteService, NoteService>();


builder.Services.AddScoped<IImageUtilities, ImageUtilities>();


// This line of code make UserManager work so that it returns a user instead of null
builder.Services.Configure<IdentityOptions>(options => options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

builder.Services.AddHttpContextAccessor();
builder.Services.AddAzureClients(clientBuilder =>
{
    clientBuilder.AddBlobServiceClient(builder.Configuration["BlobStorage:blob"], preferMsi: true);
    clientBuilder.AddQueueServiceClient(builder.Configuration["BlobStorage:queue"], preferMsi: true);
});

var app = builder.Build();

app.UseSwaggerUI();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
    app.UseWebAssemblyDebugging();
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseDeveloperExceptionPage();
    //app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseSwagger();

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();

app.UseRouting();

// Add CORS policy to allow access to Azure Blobs storage
// https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-6.0
app.UseCors(MyAllowedSpecificOrigins);
app.UseStaticFiles();

app.UseIdentityServer();
app.UseAuthorization();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

using (var scope = app.Services.CreateScope())
{
    var service = scope.ServiceProvider;
    var context = service.GetService<ApplicationDbContext>();
    context.Database.Migrate();
}

app.Run();

In my environmental settings the VaultUri is correctly set up:

enter image description here

In my Manage Azure App Service Settings all seems correct there as well:

enter image description here

2

Answers


  1. Chosen as BEST ANSWER

    I came back to post some more findings and to give a workaround. It seems that there is some bug in Visual Studio that is causing this issue. When I look inside my Publish settings, I see this:

    enter image description here

    Inside the Databases section, it does not show my databases. Instead is lists all the names of all the secrets in my Key Vault. Next to them, it shows the exact same secret url.

    Here is a picture of my key vault:

    enter image description here

    You can see that all the secret names match up with what presumably should be databases.

    As it happens all the secret key URLs correctly point to my 'ConnectionStrings--DefaultConnection' key. The key contains the correct connection string, but yet the publisher somehow fails to get it.

    In the end I just directly copied the connection string into the ApplicationDbContext field, and now when I publish it works fine.

    It is not ideal, but it is better than nothing. There is clearly something wrong with Visual Studio.


  2. These are the 2 lines it added:

    Yes, when you add Key Vault from Connected Services our app will be automatically configured with the default code to configure Key Vault to get secrets.

    And adds the Vault URI in the Environment Variable as shown below.

    enter image description here

    • You can check the same in the Properties => Debug => General
      enter image description here

    ‘Environment.GetEnvironmentVariable("VaultUri")’ fails and returns NULL and so my app crashes and won’t publish.

    In Blazor App the Configuration file has to be in wwwroot directory.

    enter image description here

    • We can overcome the issue in 2 ways

    Way1:

    As the VaultURI is added in Environment variable, the value is available only for the local Instance.

    • We need to add the VaultURI value to the Azure App settings as well (same as the way you have added ApplicaionDBContext) while Publishing the App.

    enter image description here

    enter image description here

    Way 2 :

    Add the Key Vault URI in appsettings.json file.

    appsettings.json file:

    {
      "VaultURI": "https://KVName.vault.azure.net/"
    }
    

    And Change line of code

    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultURI"));
    

    to

    var url = builder.Configuration.GetValue<string>("VaultURI");
    var keyVaultEndpoint = new Uri(url);
    

    Publish Result:

    enter image description here

    enter image description here

    Update

    The Appsetting which we have set in the Azure App configuration is available as APPSETTING_VaultURI under Environment Variables (KUDU).

    enter image description here

    As you are retrieving the KeyVault in Production environment,So we need to access using the below line.

    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("APPSETTING_VaultURI"));
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search