I have an angular application that is uploading files using my API.
I have a bit of code that looks like this:
var url = __env.apiUrl + 'documents/' + containerName + '/' + reference;
var formData = new FormData();
var request = {
method: 'POST',
url: url,
data: formData,
headers: {
'Content-Type': undefined
}
};
formData.append('file', file);
formData.append('metadata', JSON.stringify(metadata));
return $http(request).then(function (response) {
SimpleCache.remove(__env.apiUrl + '/documents');
listDirectiveService.refresh('document');
ngNotify.set('Your document was created.');
}, notifications.handleError).finally(function () {
onComplete();
});
On my API side, the c# used to get the file looks like this:
if (!request.Content.IsMimeMultipartContent("form-data")) throw new HttpResponseException(request.CreateResponse(HttpStatusCode.UnsupportedMediaType));
Valiate(containerName, directoryName);
var filesReadToProvider = await request.Content.ReadAsMultipartAsync();
// We assume that the form data name is file and metadata respectively
var fileStream = filesReadToProvider.Contents.SingleOrDefault(m => m.Headers.ContentDisposition.Name.Equals("\"file\"", StringComparison.OrdinalIgnoreCase));
var metaDataStream = filesReadToProvider.Contents.SingleOrDefault(m => m.Headers.ContentDisposition.Name.Equals("\"metadata\"", StringComparison.OrdinalIgnoreCase));
if (fileStream == null) throw new ArgumentNullException(nameof(fileStream));
if (metaDataStream == null) throw new ArgumentNullException(nameof(metaDataStream));
This works, but the problem is the name of the ContentDisposition.
If you notice, it looks for "file" instead of file.
Does anyone know why it has the extra quotes?
I never found out why this was happening, so I just made my function like this:
public async Task<string> CreateAsync(HttpRequestMessage request, string containerName, string directoryName, string existingUrl = "")
{
if (!request.Content.IsMimeMultipartContent("form-data")) throw new HttpResponseException(request.CreateResponse(HttpStatusCode.UnsupportedMediaType));
Valiate(containerName, directoryName);
var filesReadToProvider = await request.Content.ReadAsMultipartAsync();
// We assume that the form data name is file and metadata respectively
var fileStream = filesReadToProvider.Contents.SingleOrDefault(m =>
m.Headers.ContentDisposition.Name.Equals("\"file\"", StringComparison.OrdinalIgnoreCase) ||
m.Headers.ContentDisposition.Name.Equals("file", StringComparison.OrdinalIgnoreCase));
var metaDataStream = filesReadToProvider.Contents.SingleOrDefault(m =>
m.Headers.ContentDisposition.Name.Equals("\"metadata\"", StringComparison.OrdinalIgnoreCase) ||
m.Headers.ContentDisposition.Name.Equals("metadata", StringComparison.OrdinalIgnoreCase));
if (fileStream == null) throw new ArgumentNullException(nameof(fileStream));
if (metaDataStream == null) throw new ArgumentNullException(nameof(metaDataStream));
// Upload our file
var fileBytes = await fileStream.ReadAsByteArrayAsync();
var mediaType = fileStream.Headers.ContentType.MediaType;
var friendlyFileName = fileStream.Headers.ContentDisposition.FileName.Replace("\"", string.Empty);
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(friendlyFileName)}";
var directory = GetDirectory(containerName, directoryName);
var fileUrl = await Upload(directory, fileName, mediaType, fileBytes);
// Add our metadata
var metadata = await GetMetadataFromMemoryStreamAsync(metaDataStream);
metadata.Add("fileName", friendlyFileName);
metadata.Add("directory", directoryName);
metadata.Add("created", DateTime.UtcNow.ToString(CultureInfo.InvariantCulture));
await AddFileMetaDataAsync(fileUrl, metadata);
// Delete any existing file
if (!string.IsNullOrEmpty(existingUrl) && fileUrl != existingUrl) await DeleteAsync(containerName, directoryName, existingUrl);
return fileUrl;
}
Just to cover all bases. It isn't the best, but it should work fine.
Related
I am trying to attach large files to a ToDoTask using the Graph Api using the example in the docs for attaching large files for ToDoTask and the recommend class LargeFileUploadTask for uploading large files.
I have done this sucessfully before with attaching large files to emails and sending so i used that as base for the following method.
public async Task CreateTaskBigAttachments( string idList, string title, List<string> categories,
BodyType contentType, string content, Importance importance, bool isRemindOn, DateTime? dueTime, cAttachment[] attachments = null)
{
try
{
var _newTask = new TodoTask
{
Title = title,
Categories = categories,
Body = new ItemBody()
{
ContentType = contentType,
Content = content,
},
IsReminderOn = isRemindOn,
Importance = importance
};
if (dueTime.HasValue)
{
var _timeZone = TimeZoneInfo.Local;
_newTask.DueDateTime = DateTimeTimeZone.FromDateTime(dueTime.Value, _timeZone.StandardName);
}
var _task = await _graphServiceClient.Me.Todo.Lists[idList].Tasks.Request().AddAsync(_newTask);
//Add attachments
if (attachments != null)
{
if (attachments.Length > 0)
{
foreach (var _attachment in attachments)
{
var _attachmentContentSize = _attachment.ContentBytes.Length;
var _attachmentInfo = new AttachmentInfo
{
AttachmentType = AttachmentType.File,
Name = _attachment.FileName,
Size = _attachmentContentSize,
ContentType = _attachment.ContentType
};
var _uploadSession = await _graphServiceClient.Me
.Todo.Lists[idList].Tasks[_task.Id]
.Attachments.CreateUploadSession(_attachmentInfo).Request().PostAsync();
using (var _stream = new MemoryStream(_attachment.ContentBytes))
{
_stream.Position = 0;
LargeFileUploadTask<TaskFileAttachment> _largeFileUploadTask = new LargeFileUploadTask<TaskFileAttachment>(_uploadSession, _stream, MaxChunkSize);
try
{
await _largeFileUploadTask.UploadAsync();
}
catch (ServiceException errorGraph)
{
if (errorGraph.StatusCode == HttpStatusCode.InternalServerError || errorGraph.StatusCode == HttpStatusCode.BadGateway
|| errorGraph.StatusCode == HttpStatusCode.ServiceUnavailable || errorGraph.StatusCode == HttpStatusCode.GatewayTimeout)
{
Thread.Sleep(1000); //Wait time until next attempt
//Try again
await _largeFileUploadTask.ResumeAsync();
}
else
throw errorGraph;
}
}
}
}
}
}
catch (ServiceException errorGraph)
{
throw errorGraph;
}
catch (Exception ex)
{
throw ex;
}
}
Up to the point of creating the task everything goes well, it does create the task for the user and its properly shown in the user tasks list. Also, it does create an upload session properly.
The problem comes when i am trying to upload the large file in the UploadAsync instruction.
The following error happens.
Code: InvalidAuthenticationToken Message: Access token is empty.
But according to the LargeFileUploadTask doc , the client does not need to set Auth Headers.
param name="baseClient" To use for making upload requests. The client should not set Auth headers as upload urls do not need them.
Is not LargeFileUploadTask allowed to be used to upload large files to a ToDoTask?
If not then what is the proper way to upload large files to a ToDoTask using the Graph Api, can someone provide an example?
If you want, you can raise an issue for the same with the details here, so that they can have look: https://github.com/microsoftgraph/msgraph-sdk-dotnet-core/issues.
It seems like its a bug and they are working on it.
Temporarily I did this code to deal with the issue of the large files.
var _task = await _graphServiceClient.Me.Todo.Lists[idList].Tasks.Request().AddAsync(_newTask);
//Add attachments
if (attachments != null)
{
if (attachments.Length > 0)
{
foreach (var _attachment in attachments)
{
var _attachmentContentSize = _attachment.ContentBytes.Length;
var _attachmentInfo = new AttachmentInfo
{
AttachmentType = AttachmentType.File,
Name = _attachment.FileName,
Size = _attachmentContentSize,
ContentType = _attachment.ContentType
};
var _uploadSession = await _graphServiceClient.Me
.Todo.Lists[idList].Tasks[_task.Id]
.Attachments.CreateUploadSession(_attachmentInfo).Request().PostAsync();
// Get the upload URL and the next expected range from the response
string _uploadUrl = _uploadSession.UploadUrl;
using (var _stream = new MemoryStream(_attachment.ContentBytes))
{
_stream.Position = 0;
// Create a byte array to hold the contents of each chunk
byte[] _chunk = new byte[MaxChunkSize];
//Bytes to read
int _bytesRead = 0;
//Times the stream has been read
var _ind = 0;
while ((_bytesRead = _stream.Read(_chunk, 0, _chunk.Length)) > 0)
{
// Calculate the range of the current chunk
string _currentChunkRange = $"bytes {_ind * MaxChunkSize}-{_ind * MaxChunkSize + _bytesRead - 1}/{_stream.Length}";
//Despues deberiamos calcular el next expected range en caso de ocuparlo
// Create a ByteArrayContent object from the chunk
ByteArrayContent _byteArrayContent = new ByteArrayContent(_chunk, 0, _bytesRead);
// Set the header for the current chunk
_byteArrayContent.Headers.Add("Content-Range", _currentChunkRange);
_byteArrayContent.Headers.Add("Content-Type", _attachment.ContentType);
_byteArrayContent.Headers.Add("Content-Length", _bytesRead.ToString());
// Upload the chunk using the httpClient Request
var _client = new HttpClient();
var _requestMessage = new HttpRequestMessage()
{
RequestUri = new Uri(_uploadUrl + "/content"),
Method = HttpMethod.Put,
Headers =
{
{ "Authorization", bearerToken },
}
};
_requestMessage.Content = _byteArrayContent;
var _response = await _client.SendAsync(_requestMessage);
if (!_response.IsSuccessStatusCode)
throw new Exception("File attachment failed");
_ind++;
}
}
}
}
}
As mentioned in my Title, upon accessing Request.Form.Files, the application throws the exception
System.Io.InvalidDataException: "Multipart body length limit 16384 exceeded" in an Asp.Net Core 2.0.0 Web Application. The Application I am working on is an Asp.Net Core MVC Web Application. I am using Summernote as my WYSIWYG Text-Editor. There I enabled the Feature to upload Images (s. my Javascript). This worked fine, until I needed to implement another uploader to upload text files like .pdf, txt, doc.
The Error appears while using the summernote texteditor, either with the "standard" upload button or a custom pdf uplader button (s. C# Code). The Image functionality worked fine, until I (tried) to implement the new pdf file uploader. At first my Error looked pretty basic because it was clear, that the files I tried to upload where bigger than 16kb (Asp.Net Cores Uploadlimit), so I tried the "common" methods (s. below) to fix this error, that are found on Stack Overflow and similar sites:
services.Configure<FormOptions>(x =>
{
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = long.MaxValue;
x.MemoryBufferThreshold = 20;
});
or
public class RequestFormSizeLimitAttribute : Attribute, IAuthorizationFilter, IOrderedFilter
{
public int Order { get; }
private readonly FormOptions _formOptions;
public RequestFormSizeLimitAttribute(int valueCountLimit)
{
_formOptions = new FormOptions()
{
ValueCountLimit = valueCountLimit,
KeyLengthLimit = valueCountLimit
};
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var features = context.HttpContext.Features;
var formFeature = features.Get<IFormFeature>();
if (formFeature == null || formFeature.Form == null)
{
// Request form has not been read yet, so set the limits
features.Set(new FormFeature(context.HttpContext.Request, _formOptions));
}
}
}
or
<system.web>
<httpRuntime maxRequestLength="1048576" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="1073741824" />
</requestFiltering>
</security>
</system.webServer>
however none of these Methods fixed my Error.
First of all my Jquery Code:
//For Image Uploading
function sendFiles(file) {
var data = new FormData();
data.append("file", file);
data.append('divisionId', $('#divisionId').val());
$.ajax({
data: data,
type: "POST",
url: "/api/File/UploadImageAjax",
cache: false,
contentType: false,
processData: false,
success: function (image) {
if (image !== "Error") {
var picture = $(image);
$('#summernote').summernote("insertNode", picture[0]);
} else {
bootbox.alert("Fehler beim hochladen des Bildes.");
}
}
});
}
//For PDF-File Upload
$('#btnUpload').on('click', function (e) {
var validation = validator.form();
e.preventDefault();
if (validation) {
var data = new FormData();
var text = document.getElementById('pdfText').value;
var fileSelect = document.getElementById('pdfFile');
data.append('linkName', text);
data.append('divisionId', $('#divisionId').val());
var files = fileSelect.files;
data.append('file', files[0]);
closeModal();
$.ajax({
data: data,
type: "POST",
url: "/api/File/UploadTextFileAjax",
cache: false,
contentType: false,
processData: false,
success: function (file) {
var atag = document.createElement('a');
atag.setAttribute('href', file.absoulutePath);
atag.setAttribute('id', file.fileId);
atag.innerHtml = file.linkText;
$('#summernote').summernote('insertNode', atag);
},
error: function(respons) {
bootbox.alert("Fehler beim Speichern der Text Datei.");
}
});
}
});
And finally my C# Code from the Controller:
[HttpPost]
[RequestFormSizeLimit(valueCountLimit: 2147483647)]
public async Task<IActionResult> UploadImageAjax()
{
//Exception thrown here.
var files = Request.Form.Files;
var request = Request.Form.FirstOrDefault(d => d.Key == "divisionId");
var divisionName = await GetDivisionNameAsync(request);
if (divisionName != null)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
if (files.Count == 1 && user != null)
{
for (int i = 0; i < 1; i++)
{
var file = files[i];
if (TestImageFileName(file))
{
var path = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.ToString().Trim('"');
var fileName = $#"\images\Sportarten\{divisionName}\{path}";
if (!await _imageHandler.IsImageInDbAsync(fileName))
{
path = _hosting.WebRootPath + fileName;
using (FileStream fs = System.IO.File.Create(path))
{
await file.CopyToAsync(fs);
await fs.FlushAsync();
}
var image = new ImageViewModel { FileName = file.FileName, AbsolutePath = fileName, AspNetUserId = user.Id, FullPath = path };
var imageId = await _imageHandler.AddImageAsync(image);
var imageNode = $#"<img id='{imageId}' src='{fileName}' class='img-responsive'/>";
return Json(imageNode);
}
var id = await _imageHandler.GetImageIdbyNameAsync(path);
var node = $#"<img id='{id}' src='{fileName}' class='img-responsive'/>";
return Json(node);
}
}
}
}
return BadRequest("Error");
}
[HttpPost]
public async Task<IActionResult> UploadTextFileAjax()
{
//Exception thrown here.
var files = Request.Form.Files;
var request = Request.Form.FirstOrDefault(d => d.Key == "divisionId");
var divisionName = await GetDivisionNameAsync(request);
var linkText = Request.Form.FirstOrDefault(l => l.Key == "linkName").Value.ToString();
if (linkText != null && divisionName != null)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
if (files.Count == 1 && user != null)
{
for (int i = 0; i < 1; i++)
{
var file = files[i];
if (TestTextFileName(file))
{
var path = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.ToString().Trim('"');
var fileName = $#"\files\Sportarten\{divisionName}\{path}";
if (await _file.IsFileInDb(fileName))
{
path = _hosting.WebRootPath + fileName;
using (FileStream fs = System.IO.File.Create(path))
{
await file.CopyToAsync(fs);
await fs.FlushAsync();
}
var textFile = new FileViewModel
{
AbsolutePath = fileName,
AspNetUserId = user.Id,
FileName = file.FileName,
FullPath = path
};
var fileId = await _file.AddFile(textFile);
return Ok(new {absolutePath = path, fileId = fileId, linkText = linkText});
}
var oldText = await _file.FindFilebyName(path);
return Ok(new { absolutePath = oldText.AbsolutePath, fileId = oldText.FileId, linkText = linkText });
}
}
}
}
return BadRequest("Error");
}
At last here is my Stacktrace I get, when the error is thrown(sorry for the bad quality):
Full Stacktrace of Error
When posting an image, HttpContext.Current.Request is null.
Is there any simple way to achieve this? I am using dropzone.js on client side.
Project is Angular with Web API (ASP.NET Core 2.0) template.
[HttpPost]
public HttpResponseMessage UploadJsonFile()
{
HttpResponseMessage response = new HttpResponseMessage();
var httpRequest = HttpContext.Current.Request;
if (httpRequest.Files.Count > 0)
{
foreach (string file in httpRequest.Files)
{
var postedFile = httpRequest.Files[file];
var filePath = HttpContext.Current.Server.MapPath("~/UploadFile/" + postedFile.FileName);
postedFile.SaveAs(filePath);
}
}
return response;
}
This is the code which i had done it is working fine.
public void PostFile(IFormFile file)
{
var uploads = Path.Combine("ROOT PATH FOR THE FILES", "uploads");
if (file.Length > 0)
{
var filePath = Path.Combine(uploads, file.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
file.CopyTo(fileStream);
}
}
}
First of all we need to enable File Upload in Swagger using IOperationFilter. Create a class which inherits from IOperationFilter. For detail read this article
public class FormFileSwaggerFilter: IOperationFilter
{
private const string formDataMimeType = "multipart/form-data";
private static readonly string[] formFilePropertyNames =
typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(p => p.Name).ToArray();
public void Apply(Operation operation, OperationFilterContext context)
{
var parameters = operation.Parameters;
if (parameters == null || parameters.Count == 0) return;
var formFileParameterNames = new List<string>();
var formFileSubParameterNames = new List<string>();
foreach (var actionParameter in context.ApiDescription.ActionDescriptor.Parameters)
{
var properties =
actionParameter.ParameterType.GetProperties()
.Where(p => p.PropertyType == typeof(IFormFile))
.Select(p => p.Name)
.ToArray();
if (properties.Length != 0)
{
formFileParameterNames.AddRange(properties);
formFileSubParameterNames.AddRange(properties);
continue;
}
if (actionParameter.ParameterType != typeof(IFormFile)) continue;
formFileParameterNames.Add(actionParameter.Name);
}
if (!formFileParameterNames.Any()) return;
var consumes = operation.Consumes;
consumes.Clear();
consumes.Add(formDataMimeType);
foreach (var parameter in parameters.ToArray())
{
if (!(parameter is NonBodyParameter) || parameter.In != "formData") continue;
if (formFileSubParameterNames.Any(p => parameter.Name.StartsWith(p + "."))
|| formFilePropertyNames.Contains(parameter.Name))
parameters.Remove(parameter);
}
foreach (var formFileParameter in formFileParameterNames)
{
parameters.Add(new NonBodyParameter()
{
Name = formFileParameter,
Type = "file",
In = "formData"
});
}
}
}
Then Register this class in Startup.cs
services.AddSwaggerGen(options =>
{
// Swagger Configuration
// Register File Upload Operation Filter
options.OperationFilter<FormFileSwaggerFilter>();
});
Now Define a method as below in Service file,
public class ExampleAppService : // Inherit from required class/interface
{
public RETURN_TYPE UploadFile([FromForm]IFormFile file)
{
// Save file here
}
}
*** Don't forget to use [FromForm] in method parameter for uploading file, else you will get six more params in swagger ui.
Now, Generating Service file of angular using NSwag will require a parameter of type FileParameter. Now in component,
methodName = (file): void => {
// file is the selected file
this._service
.uploadDocument({ data: file, fileName: file.name } as FileParameter)
.subscribe((res) => {
// Handle Response
});
};
You cannot use app service for uploading image.
Just create a new controller and upload your file.
Return to client the unique filename that you generated in server.
When user saves the whole entity, send the unique filename to server again.
You can derive your controller from AbpController.
https://aspnetboilerplate.com/Pages/Documents/AspNet-Core?searchKey=AbpController
https://aspnetboilerplate.com/Pages/Documents/MVC-Controllers?searchKey=AbpController
first inject IHostingEnvironment to get server path
private IHostingEnvironment _environment;
then use it in the function
[HttpPost]
public void PostFile(IFormFile file)
{
string uploads = Path.Combine(_environment.WebRootPath, "uploads");
if (file.Length > 0)
{
var filePath = Path.Combine(uploads, file.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
file.CopyTo(fileStream);
}
}
}
I'm trying to send .csv file from my client app (angular 2) to my web api (ASP.NET), and I have done the following:
Tried to make FormData from my .csv file the following way:
public sendData() {
let formData = new FormData();
formData.append('file', this.file, this.file.name);
this.myService.postMyData(formData, this.name)
.subscribe(data => this.postData = JSON.stringify(data),
error => this.error = error,
() => console.log('Sent'));
}
Created a service on the client app where I'm sending this .csv file from.
postMyData(formData: any, name: string) {
this.s = <string><any>name;
const headers = new Headers();
headers.append('Content-Disposition', 'form-data');
const url: string = 'myUrl?methodName=' + name;
return this.http.post(url, formData, {headers: headers})
.map((res: Response) => res.json());
}
What's the problem now is that I don't know how to get that .csv file on the server. I tried it with the code found below, but I can't get the real content, I can only see the name, content type, length and stuff like that.
[HttpPost("GetMyCsvFile")]
public async Task<IActionResult> GetMyCsvFile(string name) {
var rawMessage = await Request.ReadFormAsync();
var msg = rawMessage.Files[0];
....
}
And then whatever I do with rawMessage, I can't get the content which I could read and do the stuff needed.
Is this possible to do?
You need to get the file and not the file name. Try this code, I'm getting a CSV file from my angular app.
public async Task<bool> GetFileFromAngular(IFormFile file) {
using (var reader = new StreamReader(file.OpenReadStream())) {
var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
HasHeaderRecord = true,
MissingFieldFound = null,
BadDataFound = null,
TrimOptions = TrimOptions.Trim
};
using (var csv = new CsvReader(reader, config)) {
try {
var records = csv.GetRecords<DrugFormulary>().ToList();
var csvProcessor = new CsvProcessor(_dbContext, _configuration);
await csvProcessor.ProcessPlan(records);
} catch (System.Exception ex) {
throw ex;
}
}
}
return true;
}
Solved!!! - See last edit.
In my MVC app I make calls out to a Web API service with HMAC Authentication Filterign. My Get (GetMultipleItemsRequest) works, but my Post does not. If I turn off HMAC authentication filtering all of them work. I'm not sure why the POSTS do not work, but the GETs do.
I make the GET call from my code like this (this one works):
var productsClient = new RestClient<Role>(System.Configuration.ConfigurationManager.AppSettings["WebApiUrl"],
"xxxxxxxxxxxxxxx", true);
var getManyResult = productsClient.GetMultipleItemsRequest("api/Role").Result;
I make the POST call from my code like this (this one only works when I turn off HMAC):
private RestClient<Profile> profileClient = new RestClient<Profile>(System.Configuration.ConfigurationManager.AppSettings["WebApiUrl"],
"xxxxxxxxxxxxxxx", true);
[HttpPost]
public ActionResult ProfileImport(IEnumerable<HttpPostedFileBase> files)
{
//...
var postResult = profileClient.PostRequest("api/Profile", newProfile).Result;
}
My RestClient builds like this:
public class RestClient<T> where T : class
{
//...
private void SetupClient(HttpClient client, string methodName, string apiUrl, T content = null)
{
const string secretTokenName = "SecretToken";
client.BaseAddress = new Uri(_baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
if (_hmacSecret)
{
client.DefaultRequestHeaders.Date = DateTime.UtcNow;
var datePart = client.DefaultRequestHeaders.Date.Value.UtcDateTime.ToString(CultureInfo.InvariantCulture);
var fullUri = _baseAddress + apiUrl;
var contentMD5 = "";
if (content != null)
{
var json = new JavaScriptSerializer().Serialize(content);
contentMD5 = Hashing.GetHashMD5OfString(json); // <--- Javascript serialized version is hashed
}
var messageRepresentation =
methodName + "\n" +
contentMD5 + "\n" +
datePart + "\n" +
fullUri;
var sharedSecretValue = ConfigurationManager.AppSettings[_sharedSecretName];
var hmac = Hashing.GetHashHMACSHA256OfString(messageRepresentation, sharedSecretValue);
client.DefaultRequestHeaders.Add(secretTokenName, hmac);
}
else if (!string.IsNullOrWhiteSpace(_sharedSecretName))
{
var sharedSecretValue = ConfigurationManager.AppSettings[_sharedSecretName];
client.DefaultRequestHeaders.Add(secretTokenName, sharedSecretValue);
}
}
public async Task<T[]> GetMultipleItemsRequest(string apiUrl)
{
T[] result = null;
try
{
using (var client = new HttpClient())
{
SetupClient(client, "GET", apiUrl);
var response = await client.GetAsync(apiUrl).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await response.Content.ReadAsStringAsync().ContinueWith((Task<string> x) =>
{
if (x.IsFaulted)
throw x.Exception;
result = JsonConvert.DeserializeObject<T[]>(x.Result);
});
}
}
catch (HttpRequestException exception)
{
if (exception.Message.Contains("401 (Unauthorized)"))
{
}
else if (exception.Message.Contains("403 (Forbidden)"))
{
}
}
catch (Exception)
{
}
return result;
}
public async Task<T> PostRequest(string apiUrl, T postObject)
{
T result = null;
try
{
using (var client = new HttpClient())
{
SetupClient(client, "POST", apiUrl, postObject);
var response = await client.PostAsync(apiUrl, postObject, new JsonMediaTypeFormatter()).ConfigureAwait(false); //<--- not javascript formatted
response.EnsureSuccessStatusCode();
await response.Content.ReadAsStringAsync().ContinueWith((Task<string> x) =>
{
if (x.IsFaulted)
throw x.Exception;
result = JsonConvert.DeserializeObject<T>(x.Result);
});
}
}
catch (HttpRequestException exception)
{
if (exception.Message.Contains("401 (Unauthorized)"))
{
}
else if (exception.Message.Contains("403 (Forbidden)"))
{
}
}
catch (Exception)
{
}
return result;
}
//...
}
My Web API Controller is defined like this:
[SecretAuthenticationFilter(SharedSecretName = "xxxxxxxxxxxxxxx", HmacSecret = true)]
public class ProfileController : ApiController
{
[HttpPost]
[ResponseType(typeof(Profile))]
public IHttpActionResult PostProfile(Profile Profile)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
GuidValue = Guid.NewGuid();
Resource res = new Resource();
res.ResourceId = GuidValue;
var data23 = Resourceservices.Insert(res);
Profile.ProfileId = data23.ResourceId;
_profileservices.Insert(Profile);
return CreatedAtRoute("DefaultApi", new { id = Profile.ProfileId }, Profile);
}
}
Here is some of what SecretAuthenticationFilter does:
//now try to read the content as string
string content = actionContext.Request.Content.ReadAsStringAsync().Result;
var contentMD5 = content == "" ? "" : Hashing.GetHashMD5OfString(content); //<-- Hashing the non-JavaScriptSerialized
var datePart = "";
var requestDate = DateTime.Now.AddDays(-2);
if (actionContext.Request.Headers.Date != null)
{
requestDate = actionContext.Request.Headers.Date.Value.UtcDateTime;
datePart = requestDate.ToString(CultureInfo.InvariantCulture);
}
var methodName = actionContext.Request.Method.Method;
var fullUri = actionContext.Request.RequestUri.ToString();
var messageRepresentation =
methodName + "\n" +
contentMD5 + "\n" +
datePart + "\n" +
fullUri;
var expectedValue = Hashing.GetHashHMACSHA256OfString(messageRepresentation, sharedSecretValue);
// Are the hmacs the same, and have we received it within +/- 5 mins (sending and
// receiving servers may not have exactly the same time)
if (messageSecretValue == expectedValue
&& requestDate > DateTime.UtcNow.AddMinutes(-5)
&& requestDate < DateTime.UtcNow.AddMinutes(5))
goodRequest = true;
Any idea why HMAC doesn't work for the POST?
EDIT:
When SecretAuthenticationFilter tries to compare the HMAC sent, with what it thinks the HMAC should be they don't match. The reason is the MD5Hash of the content doesn't match the MD5Hash of the received content. The RestClient hashes the content using a JavaScriptSerializer.Serialized version of the content, but then the PostRequest passes the object as JsonMediaTypeFormatted.
These two types don't get formatted the same. For instance, the JavaScriptSerializer give's us dates like this:
\"EnteredDate\":\"\/Date(1434642998639)\/\"
The passed content has dates like this:
\"EnteredDate\":\"2015-06-18T11:56:38.6390407-04:00\"
I guess I need the hash to use the same data that's passed, so the Filter on the other end can confirm it correctly. Thoughts?
EDIT:
Found the answer, I needed to change the SetupClient code from using this line:
var json = new JavaScriptSerializer().Serialize(content);
contentMD5 = Hashing.GetHashMD5OfString(json);
To using this:
var json = JsonConvert.SerializeObject(content);
contentMD5 = Hashing.GetHashMD5OfString(json);
Now the sent content (formatted via JSON) will match the hashed content.
I was not the person who wrote this code originally. :)
Found the answer, I needed to change the SetupClient code from using this line:
var json = new JavaScriptSerializer().Serialize(content);
contentMD5 = Hashing.GetHashMD5OfString(json);
To using this:
var json = JsonConvert.SerializeObject(content);
contentMD5 = Hashing.GetHashMD5OfString(json);
Now the content used for the hash will be formatted as JSON and will match the sent content (which is also formatted via JSON).