My Blazor WASM (hosted) app is spending quite a long time (~10s) in the authentication process when I open the webpage for the first time and eventually logs
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Afterwards, it continues to show all content which is not wrapped in an Authorized
tag.
In App.razor
I use the Authorizing
component and it shows its content ("Determining session state...") for a long time until it continues (see App.razor
below).
The authentication in my app happens through an endpoint /authentication/login
which forwards the user to Auth0 as identity provider using the
<RemoteAuthenticatorView Action="@Action">
component and the configuration in Program.cs
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Auth0", options.ProviderOptions);
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.AdditionalProviderParameters.Add(
"audience", builder.Configuration["Auth0:Audience"]);
});
App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Determining session state, please wait...</p>
</Authorizing>
<NotAuthorized>
<h1>Sorry</h1>
<p>You are not authorized to view this page</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
The issue is caused by a timeout in the underlying implementation of the authentication services. I traced down the source, but there's no easy solution to this issue.
If you enable Debug tracing for your WASM client, you should see this log message in the console:
dbug: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0] Initial silent sign in failed 'Frame window timed out'
For me - using Keycloak (instead of Auth0), and Discord as IdP behind Keycloak - the Discord login cannot be framed in the hidden iframe:
Refused to frame 'https://discord.com/' because it violates the following Content Security Policy directive: "frame-src 'self' my.domain.com".
Of course this policy can be modified to include discord.com
, but Discord denies being embedded that way with X-Frame-Options
header.
AuthorizeViewCore
is being rendered, entering OnParametersSetAsync
:
// Clear the previous result of authorization
// This will cause the Authorizing state to be displayed until the authorization has been completed
isAuthorized = null;
currentAuthenticationState = await AuthenticationState;
isAuthorized = await IsAuthorizedAsync(currentAuthenticationState.User);
AuthenticationState
is initialized by RemoteAuthenticationService.GetAuthenticationStateAsync
:
new AuthenticationState(await GetUser(useCache: true));
GetAuthenticatedUser
:
/// <summary>
/// Gets the current authenticated used using JavaScript interop.
/// </summary>
/// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns>
protected internal virtual async ValueTask<ClaimsPrincipal> GetAuthenticatedUser()
{
await EnsureAuthService();
var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser");
var user = await AccountClaimsPrincipalFactory.CreateUserAsync(account, Options.UserOptions);
return user;
}
AuthenticationService.getUser
will invoke trySilentSignIn
:
async trySilentSignIn() {
if (!this._intialSilentSignIn) {
this._intialSilentSignIn = (async () => {
try {
this.debug('Beginning initial silent sign in.');
await this._userManager.signinSilent();
this.debug('Initial silent sign in succeeded.');
} catch (e) {
if (e instanceof Error) {
this.debug(`Initial silent sign in failed '${e.message}'`);
}
// It is ok to swallow the exception here.
// The user might not be logged in and in that case it
// is expected for signinSilent to fail and throw
}
})();
}
return this._intialSilentSignIn;
}
await this._userManager.signinSilent();
will invoke the oidc-client-js UserManager signinSilent
and then _signinSilentIframe
:
_signinSilentIframe(args = {}) {
let url = args.redirect_uri || this.settings.silent_redirect_uri || this.settings.redirect_uri;
if (!url) {
Log.error("UserManager.signinSilent: No silent_redirect_uri configured");
return Promise.reject(new Error("No silent_redirect_uri configured"));
}
args.redirect_uri = url;
args.prompt = args.prompt || "none";
return this._signin(args, this._iframeNavigator, {
startUrl: url,
silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
}).then(user => {
if (user) {
if (user.profile && user.profile.sub) {
Log.info("UserManager.signinSilent: successful, signed in sub: ", user.profile.sub);
}
else {
Log.info("UserManager.signinSilent: no sub");
}
}
return user;
});
}
IFrameWindow.js
, which has a timeout of 10000 ms configured:
const DefaultTimeout = 10000;
_timeout() {
Log.debug("IFrameWindow.timeout");
this._error("Frame window timed out");
}