admin管理员组

文章数量:1122832

I'm developing an application using .NET 9.0 and Blazor Server-Side, and I'm using the Refit NuGet package to make API calls.

Some of API methods require a JWT access token for authorization. This token needs to be refreshed with refresh token and is stored in ProtectedLocalStorage

First solution

To handle this, I use [Header("Authentication: Bearer")] attributes in client interfaces. For example:

[Post("/")]
[Headers("Authorization: Bearer")]
Task<IApiResponse<PersonResponse>> PostPerson([Body] PersonRequest body);

In RefitSettings, I set the AuthorizationHeaderValueGetter as: (_, cancellationToken) => AuthenticationTokenProvider.GetTokenAsync(cancellationToken)

The AuthenticationTokenProvider is a static class that looks like this:

public static class AuthenticationTokenProvider
{
    private static Func<CancellationToken, Task<string>>? _getTokenAsyncFunc;
    
    public static void SetTokenGetterFunc(Func<CancellationToken, Task<string>> getTokenAsyncFunc)
    {
        _getTokenAsyncFunc = getTokenAsyncFunc;
    }

    public static Task<string> GetTokenAsync(CancellationToken cancellationToken)
    {
        if (_getTokenAsyncFunc is null)
        {
            throw new InvalidOperationException("Token getter func must be set before using it");
        }
        return _getTokenAsyncFunc!(cancellationToken);
    }
}

In Program.cs, SetTokenGetterFunc is called after the application is built:

AuthenticationTokenProvider.SetTokenGetterFunc(_ =>
{
    using (var scope = app.Services.CreateScope())
    {
        IAuthenticationService service = scope.ServiceProvider.GetRequiredService<IAuthenticationService>();
        return service.GetRawAccessTokenAsync();
    }
});

The relevant services are registered as:

services.AddScoped<ITokensService, TokensService>();
services.AddScoped<IAuthenticationService, AuthenticationService>();

Here’s how AuthenticationService looks:

public class AuthenticationService : IAuthenticationService
{
    private readonly ITokensService _tokensService;
    private readonly IAuthenticationClient _authenticationClient;

    public AuthenticationService(ITokensService tokensService, IAuthenticationClient authenticationClient)
    {
        _tokensService = tokensService;
        _authenticationClient = authenticationClient;
    }

    public async Task<string> GetRawAccessTokenAsync()
    {
        string? accessToken = await _tokensService.GetAccessToken();
        if (!string.IsNullOrWhiteSpace(accessToken) && ValidateToken(accessToken))
        {
            return accessToken;
        }

        string? refreshToken = await _tokensService.GetRefreshToken();
        if (string.IsNullOrWhiteSpace(refreshToken))
        {
            return string.Empty;
        }

        IApiResponse<AccountAuthenticateResponse> refreshResponse = await _authenticationClient.AuthenticateRefresh(new AccountAuthenticateRefreshRequest
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken
        });
        if (refreshResponse.IsSuccessful)
        {
            await UpdateTokens(refreshResponse.Content);
            return refreshResponse.Content.AccessToken;
        }
        return string.Empty;
    }  

    private async Task UpdateTokens(AccountAuthenticateResponse tokens) => await Task.WhenAll(
    [
        _tokensService.SetAccessToken(tokens.AccessToken), 
        _tokensService.SetRefreshToken(tokens.RefreshToken)
    ]);

    private static bool ValidateToken(string token)
    {
        IEnumerable<Claim> claims = GetClaimsFromToken(token);
        Claim? claim = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp);
        if (claim is null || !long.TryParse(claim.Value, out long expiration))
        {
            return false;
        }
        DateTime expirationDate = DateTime.MinValue.AddTicks(expiration);
        return expirationDate > DateTime.UtcNow;
    }
    
    private static IEnumerable<Claim> GetClaimsFromToken(string token)
    {
        string payload = token.Split('.')[1];

        switch (payload.Length % 4)
        {
            case 2: payload += "=="; break;
            case 3: payload += "="; break;
        }

        byte[] jsonBytes = Convert.FromBase64String(payload);
        Dictionary<string, object>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

        if (keyValuePairs is null)
        {
            return [];
        }

        return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
    }
}

TokensService:

public class TokensService : ITokensService
{
    private readonly ILogger<TokensService> _logger;
    private readonly ProtectedLocalStorage _localStorageService;
    private readonly Configuration.Tokens _configuration;

    public TokensService(ILogger<TokensService> logger, IConfiguration configuration, ProtectedLocalStorage localStorageService)
    {
        _logger = logger;
        _localStorageService = localStorageService;
        _configuration = configuration.GetSection("Tokens").Get<Configuration.Tokens>()!;
    }

    public async Task<string?> GetAccessToken() => await GetValueAsync<string>(_configuration.StorageKeys.AccessToken);
    
    public async Task SetAccessToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.AccessToken, accessToken);

    public async Task DeleteAccessToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.AccessToken);
    
    public async Task<string?> GetRefreshToken() => await GetValueAsync<string>(_configuration.StorageKeys.RefreshToken);
    
    public async Task SetRefreshToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.AccessToken, accessToken);

    public async Task DeleteRefreshToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.RefreshToken);
    
    private async Task<T?> GetValueAsync<T>(string key)
    {
        try
        {
            ProtectedBrowserStorageResult<T> result = await _localStorageService.GetAsync<T>(key);
            return result.Success ? result.Value : default;
        }
        catch (CryptographicException ex)
        {
            _logger.LogError(ex, "Browser storage error has occurred. Deleting value.");
            await _localStorageService.DeleteAsync(key);
            return default;
        }
    }
}

So process of obtaining an access token looks like this:

  1. Get access token from local storage. If token is not empty and not expired, it is returned.
  2. Otherwise, get refresh token from local storage. If it's empty, return empty string.
  3. Send request with both tokens. If access and refresh token is valid (access token's expiration date is not checked, refresh token has to exist in database and be assigned to access token's user), new access token is returned in response. Endpoint does not require authorization header.
  4. If response is successful, save tokens and return new access token. Otherwise return empty string.

Now, when I'm trying to execute method, which require header, in OnAfterRenderAsync method of component, I'm getting this exception

System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

What's interesting is that I can execute _tokensService.GetAccessToken() directly in OnAfterRenderAsync method, but if I remove all lines, except first one (string? accessToken = await _tokensService.GetAccessToken();) in GetRawAccessTokenAsync method, I'm still getting this exception.

Second solution

I also tried to add header with DelegatingHandler. I created new class:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly IAuthenticationService _authenticationService;

    public AuthenticationHandler(IAuthenticationService authenticationService)
    {
        _authenticationService = authenticationService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string token = await _authenticationService.GetRawAccessTokenAsync();
        if (!string.IsNullOrWhiteSpace(token))
        {
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }
        return await base.SendAsync(request, cancellationToken);
    }
}

I'm registering services like this:

services.AddTransient<AuthenticationHandler>();
services.AddRefitClient<IAuthenticationClient>(settings).ConfigureHttpClient(x => x.BaseAddress = BaseUriFunc("authentication"));
services.AddRefitClient<IGenresClient>(settings).ConfigureHttpClient(x => x.BaseAddress = BaseUriFunc("genres")).AddHttpMessageHandler<AuthenticationHandler>();

But in this case I'm also getting the same exception.

Of course I removed [Headers("Authorization: Bearer")] attribute from client methods.

Edit 1

I just noticed that problems also occurs outside OnAfterRenderAsync. For example when I tried execute API client method in method, assigned to @onclick event of a Button.

I'm developing an application using .NET 9.0 and Blazor Server-Side, and I'm using the Refit NuGet package to make API calls.

Some of API methods require a JWT access token for authorization. This token needs to be refreshed with refresh token and is stored in ProtectedLocalStorage

First solution

To handle this, I use [Header("Authentication: Bearer")] attributes in client interfaces. For example:

[Post("/")]
[Headers("Authorization: Bearer")]
Task<IApiResponse<PersonResponse>> PostPerson([Body] PersonRequest body);

In RefitSettings, I set the AuthorizationHeaderValueGetter as: (_, cancellationToken) => AuthenticationTokenProvider.GetTokenAsync(cancellationToken)

The AuthenticationTokenProvider is a static class that looks like this:

public static class AuthenticationTokenProvider
{
    private static Func<CancellationToken, Task<string>>? _getTokenAsyncFunc;
    
    public static void SetTokenGetterFunc(Func<CancellationToken, Task<string>> getTokenAsyncFunc)
    {
        _getTokenAsyncFunc = getTokenAsyncFunc;
    }

    public static Task<string> GetTokenAsync(CancellationToken cancellationToken)
    {
        if (_getTokenAsyncFunc is null)
        {
            throw new InvalidOperationException("Token getter func must be set before using it");
        }
        return _getTokenAsyncFunc!(cancellationToken);
    }
}

In Program.cs, SetTokenGetterFunc is called after the application is built:

AuthenticationTokenProvider.SetTokenGetterFunc(_ =>
{
    using (var scope = app.Services.CreateScope())
    {
        IAuthenticationService service = scope.ServiceProvider.GetRequiredService<IAuthenticationService>();
        return service.GetRawAccessTokenAsync();
    }
});

The relevant services are registered as:

services.AddScoped<ITokensService, TokensService>();
services.AddScoped<IAuthenticationService, AuthenticationService>();

Here’s how AuthenticationService looks:

public class AuthenticationService : IAuthenticationService
{
    private readonly ITokensService _tokensService;
    private readonly IAuthenticationClient _authenticationClient;

    public AuthenticationService(ITokensService tokensService, IAuthenticationClient authenticationClient)
    {
        _tokensService = tokensService;
        _authenticationClient = authenticationClient;
    }

    public async Task<string> GetRawAccessTokenAsync()
    {
        string? accessToken = await _tokensService.GetAccessToken();
        if (!string.IsNullOrWhiteSpace(accessToken) && ValidateToken(accessToken))
        {
            return accessToken;
        }

        string? refreshToken = await _tokensService.GetRefreshToken();
        if (string.IsNullOrWhiteSpace(refreshToken))
        {
            return string.Empty;
        }

        IApiResponse<AccountAuthenticateResponse> refreshResponse = await _authenticationClient.AuthenticateRefresh(new AccountAuthenticateRefreshRequest
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken
        });
        if (refreshResponse.IsSuccessful)
        {
            await UpdateTokens(refreshResponse.Content);
            return refreshResponse.Content.AccessToken;
        }
        return string.Empty;
    }  

    private async Task UpdateTokens(AccountAuthenticateResponse tokens) => await Task.WhenAll(
    [
        _tokensService.SetAccessToken(tokens.AccessToken), 
        _tokensService.SetRefreshToken(tokens.RefreshToken)
    ]);

    private static bool ValidateToken(string token)
    {
        IEnumerable<Claim> claims = GetClaimsFromToken(token);
        Claim? claim = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp);
        if (claim is null || !long.TryParse(claim.Value, out long expiration))
        {
            return false;
        }
        DateTime expirationDate = DateTime.MinValue.AddTicks(expiration);
        return expirationDate > DateTime.UtcNow;
    }
    
    private static IEnumerable<Claim> GetClaimsFromToken(string token)
    {
        string payload = token.Split('.')[1];

        switch (payload.Length % 4)
        {
            case 2: payload += "=="; break;
            case 3: payload += "="; break;
        }

        byte[] jsonBytes = Convert.FromBase64String(payload);
        Dictionary<string, object>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);

        if (keyValuePairs is null)
        {
            return [];
        }

        return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
    }
}

TokensService:

public class TokensService : ITokensService
{
    private readonly ILogger<TokensService> _logger;
    private readonly ProtectedLocalStorage _localStorageService;
    private readonly Configuration.Tokens _configuration;

    public TokensService(ILogger<TokensService> logger, IConfiguration configuration, ProtectedLocalStorage localStorageService)
    {
        _logger = logger;
        _localStorageService = localStorageService;
        _configuration = configuration.GetSection("Tokens").Get<Configuration.Tokens>()!;
    }

    public async Task<string?> GetAccessToken() => await GetValueAsync<string>(_configuration.StorageKeys.AccessToken);
    
    public async Task SetAccessToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.AccessToken, accessToken);

    public async Task DeleteAccessToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.AccessToken);
    
    public async Task<string?> GetRefreshToken() => await GetValueAsync<string>(_configuration.StorageKeys.RefreshToken);
    
    public async Task SetRefreshToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.AccessToken, accessToken);

    public async Task DeleteRefreshToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.RefreshToken);
    
    private async Task<T?> GetValueAsync<T>(string key)
    {
        try
        {
            ProtectedBrowserStorageResult<T> result = await _localStorageService.GetAsync<T>(key);
            return result.Success ? result.Value : default;
        }
        catch (CryptographicException ex)
        {
            _logger.LogError(ex, "Browser storage error has occurred. Deleting value.");
            await _localStorageService.DeleteAsync(key);
            return default;
        }
    }
}

So process of obtaining an access token looks like this:

  1. Get access token from local storage. If token is not empty and not expired, it is returned.
  2. Otherwise, get refresh token from local storage. If it's empty, return empty string.
  3. Send request with both tokens. If access and refresh token is valid (access token's expiration date is not checked, refresh token has to exist in database and be assigned to access token's user), new access token is returned in response. Endpoint does not require authorization header.
  4. If response is successful, save tokens and return new access token. Otherwise return empty string.

Now, when I'm trying to execute method, which require header, in OnAfterRenderAsync method of component, I'm getting this exception

System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

What's interesting is that I can execute _tokensService.GetAccessToken() directly in OnAfterRenderAsync method, but if I remove all lines, except first one (string? accessToken = await _tokensService.GetAccessToken();) in GetRawAccessTokenAsync method, I'm still getting this exception.

Second solution

I also tried to add header with DelegatingHandler. I created new class:

public class AuthenticationHandler : DelegatingHandler
{
    private readonly IAuthenticationService _authenticationService;

    public AuthenticationHandler(IAuthenticationService authenticationService)
    {
        _authenticationService = authenticationService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        string token = await _authenticationService.GetRawAccessTokenAsync();
        if (!string.IsNullOrWhiteSpace(token))
        {
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }
        return await base.SendAsync(request, cancellationToken);
    }
}

I'm registering services like this:

services.AddTransient<AuthenticationHandler>();
services.AddRefitClient<IAuthenticationClient>(settings).ConfigureHttpClient(x => x.BaseAddress = BaseUriFunc("authentication"));
services.AddRefitClient<IGenresClient>(settings).ConfigureHttpClient(x => x.BaseAddress = BaseUriFunc("genres")).AddHttpMessageHandler<AuthenticationHandler>();

But in this case I'm also getting the same exception.

Of course I removed [Headers("Authorization: Bearer")] attribute from client methods.

Edit 1

I just noticed that problems also occurs outside OnAfterRenderAsync. For example when I tried execute API client method in method, assigned to @onclick event of a Button.

Share Improve this question edited Nov 22, 2024 at 13:44 MatS2510 asked Nov 21, 2024 at 17:39 MatS2510MatS2510 531 silver badge6 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

You need to set Prerender of pages of In your App.razor Add in your section :

<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />

And in your section add:

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

本文标签: