I need the ability to share an auth token from my host app with a blazor wasm client that runs inside one of my pages. The main site is using Open Id (is an identity server). I have read and soooo many tutorials on this and have not yet gotten it to work.
The main thing I am trying to do is mimic what happens in a vanilla blazor wasm project straight out of visual studio that uses those PersistingServerAuthenticationStateProvider and such.
From what I can tell I have everything registered correctly. When I debug, I see my PersistingServerAuth object get activated and then disposed. When I navigate to the page that holds my blazor wasm, I would expect to see the markup contain the stuff. But it's missing.
On the blazor side, it looks like there is no authenticated user - I suspect because the authentication state is not being shared correctly.
Note that this setup is running inside of Orchard Core. So maybe OC is doing something that a regular asp.net core web app would do that is interrupting this state?
Here is my Startup.ConfigureServices method.
services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
services.AddCascadingAuthenticationState();
services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>();
services.AddSignalR();
services.AddHttpsRedirection(options => { options.HttpsPort = 443; });
services.AddOrchardCms()
.AddSetupFeatures("OrchardCore.AutoSetup")
.ConfigureServices(services =>
{
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(new[] {
JwtBearerDefaults.AuthenticationScheme })
.RequireAuthenticatedUser()
.Build();
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.Authority = "https://localhost:4433/";
options.RequireHttpsMetadata = true;
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new
Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = "crt_customer_portal",
ValidIssuer = "https://localhost:4433/",
ClockSkew = TimeSpan.Zero,
IssuerSigningKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_REPLACE_TODO_REPLACE")) //TODO Replace with real key
};
});
})
.Configure((app, routes) =>
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
});
Here is the startup inside my blazor wasm:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
await builder.Build().RunAsync();
Here is the PersistingAuthenticationStateProvider. It is pretty much the same as what gets built in with the startup blazor app straight from Visual Studio.
public class PersistingAuthenticationStateProvider :
ServerAuthenticationStateProvider, IDisposable
{
private Task<AuthenticationState>? _authenticationStateTask;
private readonly PersistentComponentState _state;
private readonly PersistingComponentStateSubscription _subscription;
private readonly IdentityOptions _options;
public PersistingAuthenticationStateProvider(PersistentComponentState persistentComponentState, IOptions<IdentityOptions> optionsAccessor)
{
_options = optionsAccessor.Value;
_state = persistentComponentState;
AuthenticationStateChanged += OnAuthenticationStateChanged;
_subscription = _state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
}
private async Task OnPersistingAsync()
{
if (_authenticationStateTask is null)
{
throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}().");
}
var authenticationState = await _authenticationStateTask;
var principal = authenticationState.User;
if (principal.Identity?.IsAuthenticated == true)
{
var tenantId = principal.FindFirst("TenantId")?.Value;
var userId = principal.FindFirst(_options.ClaimsIdentity.UserIdClaimType)?.Value;
var name = principal.FindFirst("name")?.Value;
var bearerToken = principal.FindFirst("bearer")?.Value;
if (userId != null && name != null && bearerToken != null)
{
_state.PersistAsJson(nameof(UserClaims), new UserClaims
{
TenantId = tenantId,
UserId = userId,
Name = name,
BearerToken = bearerToken
});
}
}
}
private void OnAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask;
}
public void Dispose()
{
_authenticationStateTask?.Dispose();
AuthenticationStateChanged -= OnAuthenticationStateChanged;
_subscription.Dispose();
}
}
Any ideas what I might be missing? Could I be running into some conflict inside of Orchard Core?
After hours of tinkering, debugging and searching, I came across the answer here: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerender?view=aspnetcore-8.0.
The section: Components embedded into pages and views (Razor Pages/MVC) has this little snippet.
For components embedded into a page or view of a Razor Pages or MVC app, you must add the Persist Component State Tag Helper with the <persist-component-state />
HTML tag inside the closing tag of the app's layout. This is only required for Razor Pages and MVC apps. For more information, see Persist Component State Tag Helper in ASP.NET Core.
I added that to the markup generated in my Orchard Core theme and the state started to persist.