Pesquisa de site

javascript - Download Seguro de Arquivo Usando Blazor Webassembly e ASP.NET Core


Consegui encontrar uma solução aqui que mostra como criar um controlador e baixar arquivos usando injeção de JS: Como alguém pode gerar e salvar um arquivo no lado do cliente usando Blazor?

No entanto, adicionar o atributo [Authorize] ao controlador bloqueia qualquer tentativa (mesmo que esteja logado) de baixar o arquivo. Eu quero que apenas pessoas autorizadas tenham acesso para baixar os arquivos.

O resto do site está usando JWT sem problemas.

Minha pergunta é: como adiciono autenticação JWT a esse recurso de download de arquivos? Ou existe uma maneira alternativa? Os arquivos estão no sistema de arquivos do servidor e a abordagem acima é muito gentil com a memória, então prefiro me manter longe de blobs.

Observação: Estou usando contas de usuário no aplicativo.

Resposta #1

Para proteger o download de um arquivo, eu uso um token de uso único enviado na URI de solicitação de download:

  1. Defina uma classe para armazenar um token único
public class OneTimeToken
{
    public string Id { get; set; }

    public string ClientId { get; set; }

    public string UserId { get; set; }

    public string Data { get; set; }
}

Prefiro armazenar tokens no banco de dados, mas você pode escolher armazená-los na memória, mas obviamente no lado do servidor.

  1. Antes de baixar, crie um token

Aqui eu uso um serviço chamando uma API para criar meu token

public class OneTimeTokenService
{
    private readonly IAdminStore<OneTimeToken> _store; // this my service calling the API
    private readonly AuthenticationStateProvider _stateProvider;
    private readonly IAccessTokenProvider _provider;
    private readonly IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> _options;

    public OneTimeTokenService(IAdminStore<OneTimeToken> store,
        AuthenticationStateProvider state,
        IAccessTokenProvider provider,
        IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> options)
    {
        _store = store ?? throw new ArgumentNullException(nameof(store));
        _stateProvider = state ?? throw new ArgumentNullException(nameof(state));
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    public async Task<string> GetOneTimeToken()
    {
        // gets the user access token
        var tokenResult = await _provider.RequestAccessToken().ConfigureAwait(false);
        tokenResult.TryGetToken(out AccessToken token);
        // gets the authentication state
        var state = await _stateProvider.GetAuthenticationStateAsync().ConfigureAwait(false);
        // creates a one time token
        var oneTimeToken = await _store.CreateAsync(new OneTimeToken
        {
            ClientId = _options.Value.ProviderOptions.ClientId,
            UserId = state.User.Claims.First(c => c.Type == "sub").Value,
            Expiration = DateTime.UtcNow.AddMinutes(1),
            Data = token.Value
        }).ConfigureAwait(false);

        return oneTimeToken.Id;
    }
}
  1. Crie o URI de download quando o usuário clicar no link de download

Aqui eu uso um botão, mas funciona com qualquer elemento HTML, você pode usar um link no lugar.

@inject OneTimeTokenService _service
<button class="btn btn-secondary" @onclick="Download" >
    <span class="oi oi-arrow-circle-top"></span><span class="sr-only">Download 
    </span>
</button>
@code {
    private async Task Download()
    {
        var token = await _service.GetOneTimeToken().ConfigureAwait(false);
        var url = $"http://locahost/stuff?otk={token}";
        await _jsRuntime.InvokeVoidAsync("open", url, "_blank").ConfigureAwait(false);
    }
}
  1. Recupere o token da URL

4.1. Adicione o pacote IdentityServer4.AccessTokenValidation ao seu projeto de API.

No método ConfigureServices da Startup, use a autenticação do IdentityServer:

services.AddTransient<OneTimeTokenService>()
    .AddAuthentication()
    .AddIdentityServerAuthentication(options =>
    {
        options.TokenRetriever = request =>
        {
            var oneTimeToken = TokenRetrieval.FromQueryString("otk")(request);
            if (!string.IsNullOrEmpty(oneTimeToken))
            {
                return request.HttpContext
                    .RequestServices
                    .GetRequiredService<OneTimeTokenService>()
                    .GetOneTimeToken(oneTimeToken);
            }
            return TokenRetrieval.FromAuthorizationHeader()(request);
        };
    });
  1. Defina um serviço para ler e consumir o token de uso único a partir do URI

O token não deve ser reutilizável, então ele é excluído a cada solicitação.
Aqui é apenas um exemplo. Se você armazenar tokens no banco de dados, pode usar um contexto EF; se estiver na memória, pode usar um cache de objeto, por exemplo.

public class OneTimeTokenService{
    private readonly IAdminStore<OneTimeToken> _store;

    public OneTimeTokenService(IAdminStore<OneTimeToken> store)
    {
        _store = store ?? throw new ArgumentNullException(nameof(store));
    }

    public string GetOneTimeToken(string id)
    {
        // gets the token.
        var token = _store.GetAsync(id, new GetRequest()).GetAwaiter().GetResult();
        if (token == null)
        {
            return null;
        }
        // deletes the token to not reuse it.
        _store.DeleteAsync(id).GetAwaiter().GetResult();
        return token.Data;
    }
}