Uploading file from Blazor WebAssembly App directly to Blob storage - file-upload

I've been trying to develop a Blazor WebAssembly app (I'm trying with both .NET Standard 2.1 and .NET 5.0) and my goal is to allow the user to select a file using InputFileand for that file to be uploaded to an Azure Blob Storage Container. I've been looking around a lot and following different guides here and there but with no success. The issues I was obtaining were usually related to security for example CORS (Although it had been fully set up), Authorization fails and System.PlatformNotSupportedException: System.Security.Cryptography.Algorithms is not supported on this platform.
Regardless of whether it is good practice or not; is it possible to directly upload to a blob storage from a blazor app? One method i tried was via a SAS token. It works via a CONSOLE APP but not a BLAZOR APP.
<label for="browseData"><b>Browse File</b></label>
<p><InputFile id="browseData" OnChange="#OnInputFileChange" /></p>
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 1;
var inputFile = e.GetMultipleFiles(maxAllowedFiles).First();
var stream = inputFile.OpenReadStream();
await StorageService.UploadFileToStorage(stream, "sftp-server", inputFile.Name);
}
Storage Service
public class AzureStorageService
{
private readonly IAzureStorageKeyService _azureStorageKeyService;
public AzureStorageService(IAzureStorageKeyService azureStorageKeyService)
{
_azureStorageKeyService = azureStorageKeyService;
}
public async Task<Uri> UploadFileToStorage(Stream stream, string container, string fileName)
{
try
{
const string REPLACE_THIS_ACCOUNT = "test";
var blobUri = new Uri("https://"
+ REPLACE_THIS_ACCOUNT +
".blob.core.windows.net/" +
container + "/" + fileName);
// Create the blob client.
AzureSasCredential azureSasCredential = new AzureSasCredential(
"?sv=2019-12-12&ss=bfqt&srt=sco&sp=rwdlacupx&se=2021-01-20T04:21:45Z&st=2021-01-19T20:21:45Z&spr=https&sig=OIkLePYDcF2AChtYUKs0VxUajs4KmwSyOXpQkFLvN2M%3D");
var blobClient = new BlobClient(blobUri, azureSasCredential);
// Upload the file
var response = await blobClient.UploadAsync(stream, true);
return blobUri;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return null;
}
}
}
Like I was mentioning this will work via a console app but not a blazor app due to CORS..is this a security feature that just cannot be bypassed and just has to be done via the server side through a function -> blob?

If you want to directly upload file to Azure blob in Blazor WebAssembly application, we need to configure CORS fro Azure storage account. Regarding how to configure it, please refer to here.
For example
Configure CORS
Allowed origins: *
Allowed verbs: DELETE,GET,HEAD,MERGE,POST,OPTIONS,PUT
Allowed headers: *
Exposed headers: *
Maximum age (seconds): 3600
Create Account SAS.
My upload file compoment
#page "/fileupload"
#using System.ComponentModel.DataAnnotations
#using System.IO
#using System.Linq
#using System.Threading
#using Azure.Storage.Blobs
#using Azure.Storage.Blobs.Models
#using Azure.Storage
#using Azure
<h3>File Upload Component</h3>
<label for="browseData"><b>Browse File</b></label>
<p><InputFile id="browseData" OnChange="#OnInputFileChange" /></p>
#{
var progressCss = "progress " + (displayProgress ? "" : "d-none");
}
<div class="#progressCss">
<div class="progress-bar" role="progressbar" style="#($"width: { progressBar }%")" aria-valuenow="#progressBar" aria-valuemin="0" aria-valuemax="100"></div>
</div>
#code {
private bool displayProgress = false;
private string result = string.Empty;
private string progressBar;
private int maxAllowedSize = 10 * 1024 * 1024;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 1;
var inputFile = e.GetMultipleFiles(maxAllowedFiles).First();
var blobUri = new Uri("https://"
+ "23storage23" +
".blob.core.windows.net/" +
"upload" + "/" + inputFile.Name);
AzureSasCredential credential = new AzureSasCredential("");
BlobClient blobClient = new BlobClient(blobUri, credential, new BlobClientOptions());
displayProgress = true;
var res = await blobClient.UploadAsync(inputFile.OpenReadStream(maxAllowedSize), new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = inputFile.ContentType },
TransferOptions = new StorageTransferOptions
{
InitialTransferSize = 1024 * 1024,
MaximumConcurrency = 10
},
ProgressHandler = new Progress<long>((progress) =>
{
progressBar = (100.0 * progress / inputFile.Size).ToString("0");
})
});
}
}
Besides, you also can download my sample to do a test.

I was able to upload a file with your code. It was working like a charm. So, there is no error in your source code. I used the Microsoft.NET.Sdk.BlazorWebAssembly SDK with net5.0 as the target framework. Probably, it's a configuration issue with the storage account.
The CORS policy allows the host of the application (https://localhost:5001) to accesses the resources with any verb and any header (*) values. The response can include the content-length header. Is your WASM self hosted or hosted within an ASP core application? You can easily spot what origin is sent to azure by inspected the request.
Based on your context, only a few verbs like PUT might be enough.
The configuration of the SAS key can also be more specific.
Using SAS from Blazor WASM
Even not entirely part of your questions, but worth mentioning. If you want to allow a direct upload into your blob storage - and there are good reasons for it - before uploading a file, your WASM app should send a request to your API, the response contains the SAS key that can only be used for this one operation. The SAS key itself should be a user delegation SAS key

Related

How to access a file server share from an ASP.NET Core web API application published in IIS within the same domain?

I need access to files that are in a files server in my LAN from my Angular app.
I assume that I need to publish my Angular app in the same network, that is, in my IIS Server inside the same LAN
Now on my local machine, I try to access my shared folder \192.168.100.7\OfertasHistoric" but I donĀ“t know how to do it.
When I try this
[HttpGet("directorio")]
public async Task<ActionResult<string[]>> GetDirectoryContents()
{
string[] files = Directory.GetFiles(#"\\192.168.100.7\ofertashistorico");
return files;
}
I get this error
System.IO.DirectoryNotFoundException: Could not find a part of the path '/Users/kintela/Repos/Intranet-WebAPI/Intranet.API/\192.168.100.7\ofertashistorico'
It seems that the path that you give to the GetFiles method only searches from the current directory where the project is located downwards and I don't know how to indicate a different one.
I also do not know how to manage the issue of the credentials necessary to access said resource
Any idea, please?
Thanks
I am using below code and it works for me. Please check it.
Steps:
Navigate to the path like : \\192.168.2.50\ftp
Delete \ftp, the address in folder explorer should be \\192.168.2.50, find the folder you want, right click and map network drive.
You can try it with this address ftp:\\192.168.2.50, it will pop up a window. Input you usename and password, then you can check the files.
Test Result
Sample code
[HttpGet("directorio")]
public IActionResult GetDirectoryContents()
{
string networkPath = #"ftp:\\192.168.2.50";
string userName = #"Administrator";
string password = "Yy16";
#region FtpWebRequest
var networkCredential = new NetworkCredential(userName, password);
var uri = new Uri(networkPath);
var request = (FtpWebRequest)WebRequest.Create(uri);
request.Credentials = networkCredential;
request.Method = WebRequestMethods.Ftp.ListDirectory;
try
{
using (var response = (FtpWebResponse)request.GetResponse())
{
using (var stream = response.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine(reader.ReadToEnd());
}
}
}
}
catch (WebException ex)
{
Console.WriteLine("Access to the path '" + networkPath + "' is denied. Error message: " + ex.Message);
}
#endregion
return Ok();
}

App stops working when calling Http.PostAsync when returning a string

I have a Blazor app that's calling a WebAPI method that returns a string, however when it runs, it just stops. The app remains open but nothing appears to happen.
When i put a breakpoint in the controller it does run.
var ms = new System.IO.MemoryStream();
await file.Data.CopyToAsync(ms);
status = $"Finished loading {file.Size} bytes from {file.Name}";
var content = new MultipartFormDataContent {
{ new ByteArrayContent(ms.GetBuffer()), "\"upload\"", file.Name }
};
string featureImage = await Http.PostAsync("api/Blog/ReturnString", content).Result.Content.ReadAsStringAsync();
Above is the code that calls the WebAPI method. Below is the Controller that returns a very simple string.
[Authorize]
[HttpPost("[action]")]
public async Task<string> ReturnString()
{
return "123456";
}
Apart from the comment by #enet, there're likely two more bugs:
There's a risk of dead lock in the way you get Result by using .Result.Content. You need change the code to await ...
Also, if you're using Blazor Server side, be aware that invoking Http in Blazor ServerSide App is different from the Blazor WebAssembly App. You need create your own HttpClient or inject one and then provide base uri for it.
Hope the below helps:
(if you're using Blazor ServerSide) register HttpClient related services in your startup:
services.AddHttpClient(); // only if you're using Blazor ServerSide
And within your Blazor component/page:
#using System.Net.Http
#inject IHttpClientFactory HttpClientFactory #*if you're using Blazor ServerSide*#
#inject NavigationManager navMgr #*if you're using Blazor ServerSide*#
#code{
var Http = HttpClientFactory.CreateClient(); //if you're using Blazor ServerSide
Http.BaseAddress = new Uri(navMgr.BaseUri); //if you're using Blazor ServerSide
// ... add credentials if you're using Blazor ServerSide
//... your code
// don't block it by access the`.Result` property, await it!
await Http.PostAsync("api/Blog/ReturnString", content).Result.Content.ReadAsStringAsync();
var resp= await Http.PostAsync("api/Blog/ReturnString", content);
var featureImage = await resp.Content.ReadAsStringAsync();
Finally, if above code doesn't address the problem, could you please Press F12 within the browser and show us the error message ?

Call Azure API from WebJob/ Shared code between WebJob and web api

I have a web api in an ASE and an associated web job. I am trying to call this web api from the web job but it always fails with winhttpexception: a security error has occurred. I have put in all the tls related settings but still getting the error.
Any suggestions on the error?
Also is there a way to share code between WebJob and web api?
I was able to resolve the issue by setting the below in my code.This resolved the Security Error.
using(var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender,certificate,chain,sslPolicyErrors) => true
})
You could create a console app and publish it as Azure WebJobs. For username and password you could click Get Publish Profile in your Azure webapp overview to get them.
Then you could use the following code in Console App to call your Azure Webapi.
string userName = "$xxxxxx";
string userPassword = "xxxxxxxxxxxxx";
string webAppName = "xxxxxx";
var base64Auth = Convert.ToBase64String(Encoding.Default.GetBytes($"{userName}:{userPassword}"));
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Basic " + base64Auth);
var baseUrl = new Uri($"https://{webAppName}.azurewebsites.net/api/values");
var result = client.GetAsync(baseUrl).Result;
if (result.IsSuccessStatusCode)
{
var readTask = result.Content.ReadAsStringAsync();
readTask.Wait();
var value = readTask.Result;
Console.WriteLine(value.ToString());
}
}
Console.WriteLine("run successfully");
Output as below:

asp.net core JWT in uri query parameter?

I have an api that is protected by JWT and Authorize attribute and at the client I use jquery ajax call to deal with it.
This works fine, however I now need to be able to secure downloading of files so I can't set a header Bearer value, can it be done in the URI as an url parameter?
=-=-=-=-
UPDATE: This is what I ended up doing for my scenario which is an in-house project and very low volume but security is important and it might need to scale in future:
When user logs in I generate a random download key and put it in their user record in the db along with the expiry date of their JWT and return the download key to the client. The download route is protected to only allow a download if there is a query parameter that has the download key and that key exists in the user records and that expiry date has not passed. This way the dl key is unique per user, valid as long as the user's auth session is valid and can be revoked easily.
This is a common problem.
Whenever you want to reference images or other files directly from an API in a single page application's HTML, there isn't a way to inject the Authorization request header between the <img> or <a> element and the request to the API. You can sidestep this by using some fairly new browser features as described here, but you may need to support browsers that lack this functionality.
Fortunately, RFC 6750 specifies a way to do exactly what you're asking via the "URI Query Parameter" authentication approach. If you follow its convention, you would accept JWTs using the following format:
https://server.example.com/resource?access_token=mF_9.B5f-4.1JqM&p=q
As stated in another answer and in RFC 6750 itself, you should be doing this only when necessary. From the RFC:
Because of the security weaknesses associated with the URI method (see Section 5), including the high likelihood that the URL containing the access token will be logged, it SHOULD NOT be used unless it is impossible to transport the access token in the "Authorization" request header field or the HTTP request entity-body.
If you still decide to implement "URI Query Parameter" authentication, you can use the Invio.Extensions.Authentication.JwtBearer library and call AddQueryStringAuthentication() extension method on JwtBearerOptions. Or, if you want to do it manually, you can certainly do that as well. Here's a code sample that shows both ways as extensions of the Microsoft.AspNetCore.Authentication.JwtBearer library.
public void ConfigureServices(IServiceCollection services) {
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
options => {
var authentication = this.configuration.GetSection("Authentication");
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuers = authentication["Issuer"],
ValidAudience = authentication["ClientId"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authentication["ClientSecret"])
)
};
// OPTION 1: use `Invio.Extensions.Authentication.JwtBearer`
options.AddQueryStringAuthentication();
// OPTION 2: do it manually
options.Events = new JwtBearerEvents {
OnMessageReceived = (context) => {
StringValues values;
if (!context.Request.Query.TryGetValue("access_token", out values)) {
return Task.CompletedTask;
}
if (values.Count > 1) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"Only one 'access_token' query string parameter can be defined. " +
$"However, {values.Count:N0} were included in the request."
);
return Task.CompletedTask;
}
var token = values.Single();
if (String.IsNullOrWhiteSpace(token)) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"The 'access_token' query string parameter was defined, " +
"but a value to represent the token was not included."
);
return Task.CompletedTask;
}
context.Token = token;
return Task.CompletedTask;
}
};
}
);
}
You can use a middleware to set the authorization header from the query param:
public class SecureDownloadUrlsMiddleware
{
private readonly RequestDelegate next;
public SecureDownloadUrlsMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
// get the token from query param
var token = context.Request.Query["t"];
// set the authorization header only if it is empty
if (string.IsNullOrEmpty(context.Request.Headers["Authorization"]) &&
!string.IsNullOrEmpty(token))
{
context.Request.Headers["Authorization"] = $"Bearer {token}";
}
await next(context);
}
}
and then in Startup.cs use the middleware before the authentication middleware:
app.UseMiddleware(typeof(SecureDownloadUrlsMiddleware));
app.UseAuthentication();
Although it is technically possible to include a JWT in the URL, it is strongly discouraged. See the quote from here, which explains why it's a bad idea:
Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be
passed in page URLs (for example, as query string parameters).
Instead, bearer tokens SHOULD be passed in HTTP message headers or
message bodies for which confidentiality measures are taken. Browsers,
web servers, and other software may not adequately secure URLs in the
browser history, web server logs, and other data structures. If bearer
tokens are passed in page URLs, attackers might be able to steal them
from the history data, logs, or other unsecured locations.
However, if you have no choice or just don't care about security practices, see Technetium's answer.
If you still need it,you have to set jwt token on localStorage.After,you have to create a new header with the following code:
'functionName'():Headers{
let header =new Headers();
let token = localStorage.getItem('token')
header.append('Authorization',`Bearer ${token}`);
return header;
}
Add Hader to http requests.
return this.http.get('url',new RequestOptions({headers:this.'serviceName'.'functionName'()}))
Although this is a bit outside of the box, I would advice you to do the same as this is the best scalable solution when developing in the .NET environment.
Use Azure Storage! Or any other similar online cloud storage solution.
It makes sure your web app is separate from your files, so you don't have to worry about moving an application to a different web environment.
Web storage is mostly more expensive then azure storage (1GB with about 3000 operations (read/write/list) costs in total about $0.03.
When you scale your application where downtime is more critical, point 1 also applies when you use a swapping/staging technique.
Azure storage takes care of the expiry of so called Shared Access Tokens (SAS)
For the sake of simplicity for you, I will just include my code here so you don't have to google the rest
So what I do in my case, all my files are saved as Attachments within the database (not the actual file of course).
When someone requests an attachment, I do a quick check to see if the expire date has passed and if so we should generate a new url.
//where ever you want this to happen, in the controller before going to the client for example
private async Task CheckSasExpire(IEnumerable<AttachmentModel> attachments)
{
foreach (AttachmentModel attachment in attachments)
{
await CheckSasExpire(attachment);
}
}
private async Task CheckSasExpire(AttachmentModel attachment)
{
if (attachment != null && attachment.LinkExpireDate < DateTimeOffset.UtcNow && !string.IsNullOrWhiteSpace(attachment.AzureContainer))
{
Enum.TryParse(attachment.AzureContainer, out AzureStorage.ContainerEnum container);
string url = await _azureStorage.GetFileSasLocator(attachment.Filename, container);
attachment.FileUrl = url;
attachment.LinkExpireDate = DateTimeOffset.UtcNow.AddHours(1);
await _attachmentRepository.UpdateAsync(attachment.AttachmentId, attachment);
}
}
AzureStorage.ContainerEnum is just an internal enum to easily track the container certain files are stored in, but these can be strings of course
And my AzureStorage class:
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public async Task<string> GetFileSasLocator(string filename, ContainerEnum container, DateTimeOffset expire = default(DateTimeOffset))
{
var cont = await GetContainer(container);
CloudBlockBlob blockBlob = cont.GetBlockBlobReference(filename);
DateTimeOffset expireDate = DateTimeOffset.UtcNow.AddHours(1);//default
if (expire != default(DateTimeOffset) && expire > expireDate)
{
expireDate = expire.ToUniversalTime();
}
SharedAccessBlobPermissions permission = SharedAccessBlobPermissions.Read;
var sasConstraints = new SharedAccessBlobPolicy
{
SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-30),
SharedAccessExpiryTime = expireDate,
Permissions = permission
};
var sasToken = blockBlob.GetSharedAccessSignature(sasConstraints);
return blockBlob.Uri + sasToken;
}
private async Task<CloudBlobContainer> GetContainer(ContainerEnum container)
{
//CloudConfigurationManager.GetSetting("StorageConnectionString")
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_config["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
string containerName = container.ToString().ToLower();
CloudBlobContainer cloudContainer = blobClient.GetContainerReference(containerName);
await cloudContainer.CreateIfNotExistsAsync();
return cloudContainer;
}
So this will produce url's like so: http://127.0.0.1:10000/devstoreaccount1/invoices/NL3_2002%20-%202019-04-12.pdf?sv=2018-03-28&sr=b&sig=gSiohA%2BGwHj09S45j2Deh%2B1UYP1RW1Fx5VGeseNZmek%3D&st=2019-04-18T14%3A16%3A55Z&se=2019-04-18T15%3A46%3A55Z&sp=r
Of course you have to apply your own authentication logic when retrieving the attachments, if the user is allowed to view the file or not. But that can all be done with the JWT token and in the controller or the repository. I wouldn't worry about the URL being a public url, if one is so mighty to get that URL... within one hour... well then reduce the expire date :D

Integrating STS with AWSS3TransferManagerUploadRequest and AWSS3TransferManagerDownloadRequest

We are trying to implement AWS Security Token Service in our android and iOS app. At backend we are using below code to generate token:
public class CloudManagementImpl implements CloudManagement{
private static final Logger Log = LoggerFactory.getLogger(CloudManagementImpl.class);
#Override
public CloudConfiguration getCloudProperties() {
CloudConfiguration CloudConfiguration = new CloudConfiguration();
AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest();
assumeRoleRequest.setRoleArn(JiveGlobals.getProperty(XYZConstant.AWS_ARN_EC2_ROLE_MAP));
assumeRoleRequest.setRoleSessionName(XYZConstant.AWS_ROLE_SESSIONNAME);
assumeRoleRequest.setDurationSeconds(JiveGlobals.getIntProperty(XYZConstant.AWS_CREDENTIALS_LIFETIME, 1800));
AWSSecurityTokenServiceClient stsClient = new AWSSecurityTokenServiceClient();
AssumeRoleResult assumeRoleResult = stsClient.assumeRole(assumeRoleRequest);
if (assumeRoleResult != null) {
Credentials sessionCredentials = assumeRoleResult.getCredentials();
CloudConfiguration.setAwsAccessId(sessionCredentials.getAccessKeyId());
CloudConfiguration.setAwsAccessKey(sessionCredentials.getSecretAccessKey());
CloudConfiguration.setToken(sessionCredentials.getSessionToken());
CloudConfiguration.setAwsMainBucket(JiveGlobals.getProperty(XYZConstant.AWS_MAIN_BUCKET));
} else {
Log.error("Cloud Management :: Propery values not configured ");
}
return CloudConfiguration;
}
}
Generated token is then obtained in iOS and android app through a separate web-service call.
In android we are using below code to consume retrieved token:
public S3Client(String accessKey, String secretKey, String token, String bucketName) {
super();
this.accessKey = accessKey;
this.secretKey = secretKey;
this.bucketName = bucketName;
BasicSessionCredentials basicSessionCredentials = new BasicSessionCredentials(accessKey, secretKey, token);
amazonS3Client = new AmazonS3Client(basicSessionCredentials);
}
Problem is -
We do not have android like API in AWS mobile SDK version 2 for iOS,
using which we can consume the retrieved token, perhaps the best way
to achieve this thing in iOS is through AWSCognitoCredentialsProvider,
but we are not sure.
Please suggest - what is the best way to integrate AWS Security Token Service in iOS.
You need to implement your own credentials provider by conforming to AWSCredentialsProvider. Sounds like you already have a code snippet that retrieves the temporary credentials from your server. That logic should go into your custom credentials provider. You can take a look at the implementation of AWSWebIdentityCredentialsProvider and AWSCognitoCredentialsProvider for how to implement your own credentials provider.