Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Silent sign-in with oidc-client.js causes timeout issues with some IdPs #60115

Open
danroth27 opened this issue Jan 30, 2025 · 0 comments
Open
Labels
area-blazor Includes: Blazor, Razor Components
Milestone

Comments

@danroth27
Copy link
Member

Original posted by @Herdo:

#40764 (comment)

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

  1. The app gets loaded
  2. 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);
  3. The AuthenticationState is initialized by RemoteAuthenticationService.GetAuthenticationStateAsync:
    new AuthenticationState(await GetUser(useCache: true));
  4. 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;
    }
  5. 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;
    }
  6. The 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;
        });
    }
  7. Finally, this will end up at IFrameWindow.js, which has a timeout of 10000 ms configured:
    const DefaultTimeout = 10000;
  8. The initially logged timeout error is thrown:
    _timeout() {
        Log.debug("IFrameWindow.timeout");
        this._error("Frame window timed out");
    }
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Jan 30, 2025
@javiercn javiercn added this to the Backlog milestone Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

2 participants