Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 606
Waleed Al Harthi Avatar asked Aug 16 '20 10:08

Waleed Al Harthi


People also ask

How secure is Blazor WebAssembly?

Blazor WebAssembly apps are secured in the same manner as single-page applications (SPAs). There are several approaches for authenticating users to SPAs, but the most common and comprehensive approach is to use an implementation based on the OAuth 2.0 protocol, such as OpenID Connect (OIDC).

How do I download from Blazor WebAssembly?

In order to download file you have to use Microsoft JSInterop. There are many ways to implement your request. One way that i use is get the file as byte array then convert to base64string. Finally call the function that you created in javascript from server.

How do I add authentication to Blazor WebAssembly?

To create a new Blazor WebAssembly project with an authentication mechanism: After choosing the Blazor WebAssembly App template in the Create a new ASP.NET Core Web Application dialog, select Change under Authentication. Select Individual User Accounts to use ASP.NET Core's Identity system.

Does Blazor work with .NET core?

Blazor Server provides support for hosting Razor components on the server in an ASP.NET Core app. UI updates are handled over a SignalR connection. The runtime stays on the server and handles: Executing the app's C# code.


1 Answers

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;
    }
}
like image 148
agua from mars Avatar answered Oct 01 '22 14:10

agua from mars