Description
Original posted by @Herdo:
The current implementation with iframes and silent signin based on oidc-client-js is causing some timeout issues with third-party IdPs. I've described the cause of this issue in this Stackoverflow question: Blazor WASM - Spending a long time initially in Authorizing component. I'll add my analysis results below.
IMHO, the new solution should allow more freedom for cases like this, where you cannot influence IdP configuration like X-Frame-Options
header.
Source of issue
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.
What's happening
- The app gets loaded
AuthorizeViewCore
is being rendered, enteringOnParametersSetAsync
:// 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);
- The
AuthenticationState
is initialized byRemoteAuthenticationService.GetAuthenticationStateAsync
:new AuthenticationState(await GetUser(useCache: true));
- This will invoke
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 invoketrySilentSignIn
: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; }
- The
await this._userManager.signinSilent();
will invoke the oidc-client-js UserManagersigninSilent
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; }); }
- Finally, this will end up at
IFrameWindow.js
, which has a timeout of 10000 ms configured:const DefaultTimeout = 10000;
- The initially logged timeout error is thrown:
_timeout() { Log.debug("IFrameWindow.timeout"); this._error("Frame window timed out"); }