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

Refresh token during http request in Blazor Interactive Server with OIDC #55213

Open
1 task done
bpsc-wkubis opened this issue Apr 19, 2024 · 6 comments
Open
1 task done
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-server-auth

Comments

@bpsc-wkubis
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

In the Blazor Web App (Interactive server), the token refresh process occurs during the OnValidatePrincipal cookie event. This event is triggered if the access token is less than 5 minutes away from expiration. However, this event only executes during a complete page reload.

A potential issue arises when a user reloads the page 6 minutes prior to the access token's expiration. In this case, the OnValidatePrincipal event does not refresh the token. If the user continues to interact with the website without a full page reload, the token may expire after 6 minutes. Consequently, all subsequent API requests are rejected. The question is how to handle such a scenario.

Describe the solution you'd like

The common solution to this issue is to refresh the token during an HTTP request with a DelegatingHandler. However, in our scenario, we cannot override the cookie inside the DelegatingHandler. The expected behavior, therefore, is to be able to refresh tokens in the DelegatingHandler while storing them in cookies. If there's a way to override the cookie inside the DelegatingHandler that I'm not aware of, that could also be a potential solution.

Additional context

cc: @guardrex dotnet/blazor-samples#267
https://github.com/dotnet/blazor-samples/tree/main/8.0/BlazorWebAppOidc

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Apr 19, 2024
@javiercn javiercn added the area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer label Apr 21, 2024
@halter73
Copy link
Member

However, this event only executes during a complete page reload.

A potential issue arises when a user reloads the page 6 minutes prior to the access token's expiration. In this case, the OnValidatePrincipal event does not refresh the token.

I recommend looking at the https://github.com/dotnet/blazor-samples/tree/bf387b3a9acafad1f5d9b8403c918b3548f81906/8.0/BlazorWebAppOidcBff sample. OnValidatePrincipal be fired at the beginning of the request any time you use HttpClient in the WebAssembly app to make a request that is proxied by YARP with the access token.

If the issue is that a server rather than the client interactive session is outlasting the access token in the cookie, I think you might need some JS code that pings the server using fetch on an interval that's less than the expiration of the access token to keep the cookie up to date. You can also make the 5 minute interval longer so you don't need to ping as often.

You could copy the logic from CookieOidcRefresher into a RevalidatingServerAuthenticationStateProvider to get a new access token, but you cannot set a cookie from an interactive context. You need to somehow induce the client to make a new HTTP request to reissue a cookie.

@bpsc-wkubis
Copy link
Author

bpsc-wkubis commented Apr 23, 2024

I recommend looking at the https://github.com/dotnet/blazor-samples/tree/bf387b3a9acafad1f5d9b8403c918b3548f81906/8.0/BlazorWebAppOidcBff sample. OnValidatePrincipal be fired at the beginning of the request any time you use HttpClient in the WebAssembly app to make a request that is proxied by YARP with the access token.

To clarify, I'm using Blazor Server with interactive rendering, not WebAssembly.

You could copy the logic from CookieOidcRefresher into a RevalidatingServerAuthenticationStateProvider to get a new access token, but you cannot set a cookie from an interactive context. You need to somehow induce the client to make a new HTTP request to reissue a cookie.

I'm fine with refreshing tokens in delegating handler, but the issue persists - the cookie can't be refreshed. Therefore, it will only function until the first complete page reload.

This issue, is an edge case where the server's interactive session outlasts the access token's expiration, I'll likely have to live with it. However, I'm open to implementing new solutions if they become available in the future :)

@mkArtakMSFT mkArtakMSFT added enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-server-auth and removed area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer labels Apr 23, 2024
@mkArtakMSFT mkArtakMSFT added this to the .NET 10 Planning milestone Apr 23, 2024
@vukasinpetrovic
Copy link

I'm currently using Blazor InteractiveServer where I connect to my external/existing API. Authentication part is SSR as in standard tempaltes, so the cookie with BearerToken is saved in the browser and is extracted with DelegatingHandler for each HttpClient call to that API.
Now, I'm stuck with thinking how to even implement that refresh token functionality as I haven't found a way to manipulate the cookie (or save a new one) while executing HttpClient cals from InteractiveAuto.
@bpsc-wkubis Did you manage to figure this out?

Any guidance or help is appreciated.

@arkiaconsulting
Copy link

@vukasinpetrovic did you try the technique used here CookieOidcRefresher
It basically benefit of the cookies authentication handler event OnValidatePrincipal in order to refresh the cookie if needed.

@bpsc-wkubis
Copy link
Author

I'm currently using Blazor InteractiveServer where I connect to my external/existing API. Authentication part is SSR as in standard tempaltes, so the cookie with BearerToken is saved in the browser and is extracted with DelegatingHandler for each HttpClient call to that API. Now, I'm stuck with thinking how to even implement that refresh token functionality as I haven't found a way to manipulate the cookie (or save a new one) while executing HttpClient cals from InteractiveAuto. @bpsc-wkubis Did you manage to figure this out?

Well, I have not, I I've set the cookie lifetime to 1 hour. I hope that users won't maintain an interactive session for such a long duration. If they do, I've implemented exception handling to refresh the page.

@vukasinpetrovic
Copy link

vukasinpetrovic commented Jul 2, 2024

@vukasinpetrovic did you try the technique used here CookieOidcRefresher It basically benefit of the cookies authentication handler event OnValidatePrincipal in order to refresh the cookie if needed.

@arkiaconsulting Thank you for the guidance. I had a lot of work during the past weeks and only now got to get back at this topic. I examined that CookieRefresher and from what I understood, OnValidatePrincipal only gets executed on page refresh or loading some non-InteractiveServer pages. If I navigate through my ServerInteractive pages and functionality, it does not get executed.

Also, I used standard httpContext.SignInAsync method to login my user and that automatically generates the cookie. But from what I saw in CookieOidcRefresher, they use validateContext.Properties.Get/StoreTokens, which does not seem to use same thing that SignInAsync sets.

Am I missing something?

EDIT 2: This is all in case my blazor app connects to external api, not when api is inside blazor server.

For context, this is my login method

    private async Task HandleLogin()
    {
        try
        {
            var loginResponse = await ApiService.TwoFactorLoginAsync(Data);
            if (loginResponse != null && !string.IsNullOrEmpty(loginResponse.AccessToken))
            {
                var claims = new List<Claim>
                {
                    new(ClaimTypes.NameIdentifier, loginResponse.User.Id.ToString()),
                    new("AccessToken", loginResponse.AccessToken),
                    new("RefreshToken", loginResponse.RefreshToken)
                };

                // Add each role to claim
                loginResponse.User.Roles.ToList().ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r.Name)));

                var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

                await HttpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);

                Navigation.NavigateTo("/");
            }
        }
        // NavigationException has to occur in order for framework to execute SSR redirection, that's just how it works
        catch (Exception e) when (e is not NavigationException)
        {
            errorMessage = e.Message;
        }

And this is my auth handler for http context that is used for all api calls

public class ApiHttpAuthHandler : DelegatingHandler
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;

    public ApiHttpAuthHandler(AuthenticationStateProvider authenticationStateProvider)
    {
        _authenticationStateProvider = authenticationStateProvider;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity.IsAuthenticated)
        {
            var token = user.FindFirst("AccessToken")?.Value;

            if (!string.IsNullOrEmpty(token))
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
            }
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

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 enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-server-auth
Projects
None yet
Development

No branches or pull requests

6 participants