How do I use Dependency Injection to access the correct scoped custom AuthenticationStateProvider from a minimal API endpoint?

Steve Nicholson 20 Reputation points
2024-04-02T19:59:16.5866667+00:00

I'm trying to implement a custom AuthenticationStateProvider to use Google authentication in my interactive server-side Blazor app. I created a button using Google's code generation:

    <script src="https://accounts.google.com/gsi/client" async></script>
    <div id="g_id_onload"
         data-client_id="973757156081-jb82o3db39b5p739mma7u35k58khm4mf.apps.googleusercontent.com"
         data-context="signin"
         data-ux_mode="popup"
         data-login_uri="https://localhost:7115/google-signin"
         data-auto_prompt="false">
    </div>
    <div class="g_id_signin"
         data-type="standard"
         data-shape="pill"
         data-theme="filled_blue"
         data-text="signin_with"
         data-size="large"
         data-logo_alignment="left">
    </div>

I created an endpoint to handle the callback from Google (this is called in Program.cs with app.MapApiExtensions();):

public static class ApiExtensions
{
    public static IEndpointRouteBuilder MapApiExtensions(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapPost("google-signin", async (
            HttpContext context,
            CustomAuthenticationStateProvider authenticationStateProvider) =>
        {
            if (context.Request.Form.TryGetValue("credential", out var jwtToken))
            {
                authenticationStateProvider.SetAuthenticationState(jwtToken!); 
            }
            else
            {
                // Handle login failure.
            }
            context.Response.Redirect("/");
            await Task.CompletedTask;
        });
        return endpoints;
    }
}

My custom AuthenticationStateProvider currently just attempts to store the user.

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal _user = new(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(_user));
    }

    public void SetAuthenticationState(string jwtToken)
    {
        var identity = new ClaimsIdentity(ParseClaimsFromJwt(jwtToken), "GoogleAuth");

        _user = new ClaimsPrincipal(identity);
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_user)));
    }

    public bool UserIsAuthenticated()
    {
        return _user.Identity is { IsAuthenticated: true };
    }

    public void ClearAuthenticationState()
    {
        _user = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_user)));
    }

    private static IEnumerable<Claim> ParseClaimsFromJwt(string jwtToken)
    {
        var claims = new JwtSecurityTokenHandler().ReadJwtToken(jwtToken).Claims;
        return claims;
    }
}

It's added to dependency injection in Program.cs:

builder.Services.AddScoped<CustomAuthenticationStateProvider>();

When SetAuthenticationState is called in the google-signin endpoint, I can see that there's a valid user with claims. When GetAuthenticationStateAsync is subsequently called from a Blazor component, the user is not authenticated. I'm guessing this is because a different instance of CustomAuthenticationStateProvider is being used in the two different places. How can I get to the CustomAuthenticationStateProvider that the Blazor components are using from the google-signin endpoint?

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,648 questions
Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,500 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Steve Nicholson 20 Reputation points
    2024-04-02T22:05:02.8233333+00:00

    After more study I think my fundamental approach is incorrect.

    0 comments No comments

  2. Bruce (SqlWork.com) 61,731 Reputation points
    2024-04-03T04:16:05.74+00:00

    the custom authentication state provider SetAuthenticationState(), is meant to be called from a Blazor component. It was really designed to handle the case where Blazor components performed login rather the hosting website. See the individual account code using Blazor components.

    A Blazor server app is single request with one HttpContext, and one scoped service instance. So middleware could update the custom provider but it would need to be on the Blazor app creation request. You are using default identity oauth with Razor page support. the logic is:

    • redirect to the oauth provider
    • after login the oauth provider redirect back to app website callback
    • the callback url builds the Authentication cookie and redirects to Blazor app hosting page setting the Authentication cookie.
    • the website renders the hosting page and Blazor script file includes. If you enabled pre-render, an Blazor app instance is created, run to produce the pre-rendered html, and shut down. You will want the Authorization State instance created for this app run to be set, so the proper html is generated.
    • the browser loads the host page and run the Blazor bootstrap script. this script creates the signal/r connection, than connects to the servers Blazor hub, which starts a new app instance. You also want this instance to have the correct Authorization State.

    the default provider, uses the httpcontext user principle (built from the Authentication cookie middleware) to set the Authentication state.

    You probably want to create a custom principle in the reply url processing before the cookie is created, rather than use a custom provider.

    0 comments No comments