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.
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).
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.
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.
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.
To secure a file download I use a one time token sent in the download request URI:
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.
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;
}
}
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);
}
}
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);
};
});
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With