Skip to content

Commit 95dd904

Browse files
committed
Warm up MSAL token pipeline at startup
Microsoft.Identity.Web builds its MergedOptions and the underlying MSAL confidential-client application lazily on the first token request, and that first initialization is not safe against a second concurrent caller: a concurrent ISAR call can observe a half-initialized configuration and fail with "No ClientId was specified.". Add a MsalWarmupHostedService that acquires a single app token for the ISAR downstream API before the server accepts requests, so the options and confidential client are fully built once. A failed warm-up (no network or invalid secret in local dev) is logged and never blocks startup.
1 parent a99223c commit 95dd904

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using Microsoft.Identity.Web;
2+
3+
namespace Api.HostedServices
4+
{
5+
/// <summary>
6+
/// Warms up the Microsoft.Identity.Web token-acquisition pipeline at startup by
7+
/// acquiring a single application (client-credentials) access token for the ISAR
8+
/// downstream API.
9+
///
10+
/// Microsoft.Identity.Web builds its <c>MergedOptions</c> and the underlying MSAL
11+
/// confidential-client application lazily on the first token request. That first
12+
/// initialization is not safe against a second concurrent caller: while one thread
13+
/// is populating the merged options, another concurrent ISAR call can observe a
14+
/// half-initialized configuration and fail with <c>"No ClientId was specified."</c>.
15+
///
16+
/// Performing one token acquisition here — before the web server starts accepting
17+
/// requests — ensures the options and confidential client are fully built once, so
18+
/// the first real mission-scheduling requests never race that initialization.
19+
/// </summary>
20+
public class MsalWarmupHostedService(
21+
ILogger<MsalWarmupHostedService> logger,
22+
IServiceScopeFactory scopeFactory,
23+
IConfiguration configuration
24+
) : IHostedService
25+
{
26+
private static readonly TimeSpan WarmupTimeout = TimeSpan.FromSeconds(30);
27+
28+
public async Task StartAsync(CancellationToken cancellationToken)
29+
{
30+
var scope = configuration["Isar:Scopes:0"];
31+
if (string.IsNullOrWhiteSpace(scope))
32+
{
33+
logger.LogWarning(
34+
"Skipping MSAL warm-up: no ISAR scope configured under 'Isar:Scopes'."
35+
);
36+
return;
37+
}
38+
39+
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(
40+
cancellationToken
41+
);
42+
timeoutCts.CancelAfter(WarmupTimeout);
43+
44+
try
45+
{
46+
using var serviceScope = scopeFactory.CreateScope();
47+
var tokenAcquisition =
48+
serviceScope.ServiceProvider.GetRequiredService<ITokenAcquisition>();
49+
50+
// Triggers the one-time build of MergedOptions and the MSAL confidential
51+
// client used for all subsequent app-token calls to ISAR.
52+
await tokenAcquisition.GetAccessTokenForAppAsync(scope);
53+
54+
logger.LogInformation(
55+
"MSAL token-acquisition pipeline warmed up for ISAR downstream API."
56+
);
57+
}
58+
catch (Exception e)
59+
{
60+
// A failed warm-up (e.g. no network or invalid secret in local dev) is not
61+
// fatal: the merged options are still initialized as a side effect, and the
62+
// token will be acquired lazily on first real use. Never block startup.
63+
logger.LogWarning(
64+
e,
65+
"MSAL warm-up did not complete successfully. Continuing startup; the token will be acquired on first use."
66+
);
67+
}
68+
}
69+
70+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
71+
}
72+
}

backend/api/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107

108108
builder.Services.AddSingleton<EventAggregatorSingletonService>();
109109

110+
builder.Services.AddHostedService<MsalWarmupHostedService>();
110111
builder.Services.AddHostedService<MqttEventHandler>();
111112
builder.Services.AddHostedService<MissionEventHandler>();
112113
builder.Services.AddHostedService<MqttService>();

0 commit comments

Comments
 (0)