question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Blazor WebAssembly Auth0 logout fails

See original GitHub issue

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

There’s a closed issue that already describes this problem here #39339. That issue was closed as being an issue with Auth0 or the linked article, but I think some of the problem lies with Blazor as well.

The problem lies in the fact that the logout process of Auth0 requires an explicit call to a logout endpoint to end the session. If this call is not made then the user is not truly logged out, and any future calls the the authorize endpoint will return a valid code without requiring a login prompt.

There is an article and a repo provided by the Auth0 folks that provides an example as to how you might try to accomplish logging out, but even within the article they acknowledge this issue.

From the article:

Disclaimer: At the time of writing, the logout function seems not to be stable due to an apparently Blazor issue. In fact, in some random circumstances, it doesn’t actually call the Auth0 logout endpoint.

What they’ve stated there, that “it doesn’t actually call the Auth0 logout endpoint” is not actually correct from what I can tell. What I believe is happening is that when the authentication/logout razor page is loaded there are two requests being sent out simultaneously:

  1. There is a request going to the Auth0 logout endpoint to close the user’s session. This request is triggered by this code block in Pages/Authentication.razor:
<RemoteAuthenticatorView Action="@Action">
    <LogOut>
        @{
            var authority = (string)Configuration["Auth0:Authority"];
            var clientId = (string)Configuration["Auth0:ClientId"];

             Navigation.NavigateTo($"{authority}/v2/logout?client_id={clientId}");
        }
    </LogOut>
</RemoteAuthenticatorView>
  1. Inside an iframe, there is an additional request being sent to the Auth0 authorize endpoint that appears to be triggered as part of the logout process.

So here’s what I think is happening.

If request 1 (the request to logout) completes first then request 2 is cancelled and logging out succeeds as expected.

If request 2 (the request to authenticate) returns before the logout endpoint has finished processing the logout request, Blazor proceeds with the authentication process and requests a token and stores it in the session. By the time the logout request finally finishes and redirects the user we’ve already retrieved and stored a new token. When the page loads it thinks we’re authenticated because we have a valid token.

If you were to delete the token and attempt another silent authentication it would fail and require a login since the session has actually been ended on the Auth0 side.

So the big question then becomes, is there somewhere we should we be triggering the call to the logout endpoint so that it doesn’t have to race against a silent authentication attempt?

Expected Behavior

The call to the remote logout endpoint should not need to race against a silent authentication attempt. There should be somewhere we can trigger post logout actions that need to occur before a silent authentication attempt is triggered.

Steps To Reproduce

  1. Use this repo
  2. Replace the credentials in the appsettings.json
  3. Run the Client project

Exceptions (if any)

No response

.NET Version

5.0.103

Anything else?

No response

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:8
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

22reactions
Hawxycommented, Dec 22, 2022

I’ve done some further investigation and the underlying problem is that the redirect really needs to happen in the middle of the logout flow, between sign out being called on the oidc-client to remove the user session, and GetUser being called. Otherwise the Auth0 session sticks around and the GetUser call results in silent auth passing:

https://github.com/dotnet/aspnetcore/blob/4dfc0a726c4a88f183b642e47d8701fb1457b71c/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs#L114-L123

There’s no callback that is invoked between those two operations.

Instead, the next best thing is passing down the end_session_endpoint to the oidc-client’s metadataSeed setting, which’ll merge with the OIDC discovery data coming from Auth0. This is identical to how standalone users of oidc-client + Auth0 would usually configure the client. You can currently achieve this via extending the OidcProviderOptions:

public static class AuthExtensions
{
    /// <summary>
    /// Adds support for Auth0 authentication for SPA applications using <see cref="Auth0OidcProviderOptions"/> and the <see cref="RemoteAuthenticationState"/>.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
    /// <param name="configure">An action that will configure the <see cref="RemoteAuthenticationOptions{TProviderOptions}"/>.</param>
    /// <returns>The <see cref="IServiceCollection"/> where the services were registered.</returns>
    public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddAuth0OidcAuthentication(this IServiceCollection services, Action<RemoteAuthenticationOptions<Auth0OidcProviderOptions>> configure)
    {
        services.TryAddEnumerable(ServiceDescriptor.Scoped<IPostConfigureOptions<RemoteAuthenticationOptions<Auth0OidcProviderOptions>>, DefaultAuth0OidcOptionsConfiguration>());
        return services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount, Auth0OidcProviderOptions>(configure);
    }
}

public class Auth0OidcProviderOptions : OidcProviderOptions
{
    public MetadataSeed MetadataSeed { get; set; } = new ();
}

public class MetadataSeed
{
    [JsonPropertyName("end_session_endpoint")]
    public string EndSessionEndpoint { get; set; } = null!;
}

// Copy/paste from Microsoft.AspNetCore.Components.WebAssembly.Authentication with the option type changed. This is needed so some additional options required for OIDC are set correctly.
internal class DefaultAuth0OidcOptionsConfiguration : IPostConfigureOptions<RemoteAuthenticationOptions<Auth0OidcProviderOptions>>
{
    private readonly NavigationManager _navigationManager;

    public DefaultAuth0OidcOptionsConfiguration(NavigationManager navigationManager) => _navigationManager = navigationManager;

    public void Configure(RemoteAuthenticationOptions<Auth0OidcProviderOptions> options)
    {
        options.UserOptions.AuthenticationType ??= options.ProviderOptions.ClientId;

        var redirectUri = options.ProviderOptions.RedirectUri;
        if (redirectUri == null || !Uri.TryCreate(redirectUri, UriKind.Absolute, out _))
        {
            redirectUri ??= "authentication/login-callback";
            options.ProviderOptions.RedirectUri = _navigationManager
                .ToAbsoluteUri(redirectUri).AbsoluteUri;
        }

        var logoutUri = options.ProviderOptions.PostLogoutRedirectUri;
        if (logoutUri == null || !Uri.TryCreate(logoutUri, UriKind.Absolute, out _))
        {
            logoutUri ??= "authentication/logout-callback";
            options.ProviderOptions.PostLogoutRedirectUri = _navigationManager
                .ToAbsoluteUri(logoutUri).AbsoluteUri;
        }
    }

    public void PostConfigure(string name, RemoteAuthenticationOptions<Auth0OidcProviderOptions> options)
    {
        if (string.Equals(name, Options.DefaultName))
        {
            Configure(options);
        }
    }
}

Then, in your Program.cs:

builder.Services.AddAuth0OidcAuthentication(options =>
{
    //...
    var authority = builder.Configuration["Auth0:Authority"];
    var clientId = builder.Configuration["Auth0:ClientId"];
    options.ProviderOptions.MetadataSeed.EndSessionEndpoint = $"{authority}/v2/logout?client_id={clientId}&returnTo={builder.HostEnvironment.BaseAddress}";
});

Testing this, logouts are now consistent.

5reactions
Hawxycommented, Apr 18, 2023

Hello thread watchers, just an update that this is now resolved: https://auth0.com/docs/authenticate/login/logout/log-users-out-of-auth0

At the moment you need to reach out to Auth0 support to get end_session_endpoint added to your metadata, with a toggle being added to the UI in the future. I’ve done this myself and can confirm it works as intended.

Once enabled you can remove the workaround and use Navigation.NavigateToLogout("authentication/logout"); with a custom redirect or any of the other dynamic auth features.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Blazor WASM Logout and .Net 7.0
Hello In .NET 6.0 there seems to be some sort of issue between blazor WASM and Auth0 when it comes to logging out....
Read more >
Securing Blazor WebAssembly Apps - Auth0 Community
Tried a number of approaches but always get the “logout-failed” call-back. Most recently with the error “The logout was not initiated from within...
Read more >
Blazor webassembly "User successfully logged out" ...
When I reproduce the problem (where the logout seems to fail) I see that there is a “Success Logout” type message followed by...
Read more >
Blazor Server and the Logout Problem
For the logout problem, this value expresses the maximum time you can tolerate that a user is logged out of a tab, but...
Read more >
Blazor WebAssembly app /authentication/logout results in " ...
Blazor WebAssembly app /authentication/logout results in "There was an error trying to log you out: ''" fail · c# · identityserver4 · logout...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found