Waleed Al Harthi
Waleed Al Harthi

Reputation: 914

Secure File Download Using Blazor Webassembly and ASP.NET Core

I've managed to find a solution here that shows how to create a controller and download files using JS injection: How can one generate and save a file client side using Blazor?

However, adding the [Authorize] attribute to the controller blocks any attempts (even if logged in) to download the file. I want authorized people only to have access to download files.

The rest of the website is using JWT without issues.

My question is how do I add JWT authentication to this file download feature? Or is there an alternative way? The files are in the file system of the server and the approach above is very kind to the memory so I prefer to stay away from blobs.

Note: I'm using in-application user accounts.

Upvotes: 6

Views: 2679

Answers (1)

agua from mars
agua from mars

Reputation: 17444

To secure a file download I use a one time token sent in the download request URI:

  1. Define a class to store one time toke
public class OneTimeToken
{
    public string Id { get; set; }

    public string ClientId { get; set; }

    public string UserId { get; set; }

    public string Data { get; set; }
}

I prefer to store tokens in DB but you can choose to store it in memory but server side obviously.

  1. Before download create a token

Here I use a service calling an API to create my 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. Create the download uri when the user click the download link

Here I use a button, but it work with a any html element, you can use a link instead.

@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. Retrieve the token from the URL

4.1. Add the package IdentityServer4.AccessTokenValidation to your API project.

In Startup ConfigureServices method use the IdentityServer authentication:

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. Define a service to read and consume the one time token from the URI

The token must not be reusable, so it's delete on each request.
Here it's just a sample. If you store tokens in DB you can use an EF context, if it's in memory, you can use an object cache for exemple.

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;
    }
}

Upvotes: 6

Related Questions