I have setup a web api to allow cross domain access with Basic Authentication. When I make a cross domain GET request to the API, it works fine and I am getting token in "Authorization" header in my custom message handler. But when initiating a cross domain POST request, I am not getting the "Authorization" header that's why unable to validate the request.
Any help would be highly appreciated.
Following is the code for my custom message handler for cross domain access.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace MyWebApi.Handlers
{
public class XHttpMethodOverrideDelegatingHandler : DelegatingHandler
{
static readonly string[] HttpOverrideMethods = { "PUT", "DELETE" };
static readonly string[] AccessControlAllowMethods = { "POST", "PUT", "DELETE" };
private const string HttpMethodOverrideHeader = "X-HTTP-Method-Override";
private const string OriginHeader = "ORIGIN";
private const string AccessControlAllowOriginHeader = "Access-Control-Allow-Origin";
private const string AccessControlAllowMethodsHeader = "Access-Control-Allow-Methods";
private const string AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers";
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authHeader = request.Headers.Authorization;
if (authHeader == null || authHeader.Scheme != "Basic" || string.IsNullOrWhiteSpace(authHeader.Parameter))
{
return CreateUnauthorizedResponse();
}
if (request.Method == HttpMethod.Post && request.Headers.Contains(HttpMethodOverrideHeader))
{
var httpMethod = request.Headers.GetValues(HttpMethodOverrideHeader).FirstOrDefault();
if (HttpOverrideMethods.Contains(httpMethod, StringComparer.InvariantCultureIgnoreCase))
request.Method = new HttpMethod(httpMethod);
}
var httpResponseMessage = base.SendAsync(request, cancellationToken);
if (request.Method == HttpMethod.Options && request.Headers.Contains(OriginHeader))
{
httpResponseMessage.Result.Headers.Add(AccessControlAllowOriginHeader, request.Headers.GetValues(OriginHeader).FirstOrDefault());
httpResponseMessage.Result.Headers.Add(AccessControlAllowMethodsHeader, String.Join(", ", AccessControlAllowMethods));
httpResponseMessage.Result.Headers.Add(AccessControlAllowHeadersHeader, HttpMethodOverrideHeader);
httpResponseMessage.Result.StatusCode = HttpStatusCode.OK;
}
//No mater what the HttpMethod (POST, PUT, DELETE), if a Origin Header exists, we need to take care of it
else if (request.Headers.Contains(OriginHeader))
{
httpResponseMessage.Result.Headers.Add(AccessControlAllowOriginHeader, request.Headers.GetValues(OriginHeader).FirstOrDefault());
}
return httpResponseMessage;
}
private Task<HttpResponseMessage> CreateUnauthorizedResponse()
{
var response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.Headers.Add("WWW-Authenticate", "Basic");
var taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>();
taskCompletionSource.SetResult(response);
return taskCompletionSource.Task;
}
}
}
And i have registered the above handler in Application_Start as follows:
namespace MyWebApi
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new {id = RouteParameter.Optional});
GlobalConfiguration.Configuration.MessageHandlers.Add(new XHttpMethodOverrideDelegatingHandler());
GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter());
}
}
}
At client side on a different domain project, I am trying to add a new record using following code.
AddUser {
var jsonData = {
"FirstName":"My First Name",
"LastName": "My Last Name",
"Email": "my.name#mydomain.com",
"Password": "MyPa$$word"
};
$.ajax({
type: "POST",
dataType: 'json',
url: "http://localhost:4655/api/user/signup",
beforeSend: function (xhr) { xhr.setRequestHeader("Authorization", "Basic xxxxxxxxxxxxxx"); },
accept: "application/json",
data: JSON.stringify(jsonData),
success: function (data) {
alert("success");
},
failure: function (errorMsg) {
alert(errorMsg);
},
error: function (onErrorMsg) {
alert(onErrorMsg.statusText);
},
statusCode: function (test) {
alert("status");
}
});
});
And following is the code for my user controller.
namespace MyWebApi.Controllers
{
public class UserController : ApiController
{
[HttpPost]
[ActionName("Adduser")]
public int Post(UserModel source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
Db.Users.Add(source);
Db.SaveChanges();
return source.UserId;
}
}
}
Thanks in advance!
I've discovered that if I include basic auth credentials in my cross-domain (POST) XHR request, the browser (IE, Chrome, Firefox) rejects the request before it ever gets to my server - and this is true even if I specify withCredentials:true in my initial $.ajax() request. I'm guessing that there's probably something in the CORS spec which requires this. But I think the short answer is that you can't specify basic auth in CORS requests.
Of course, you can get around this in other ways, by passing userids and passwords as part of the URL proper, so I'm not entirely clear what they think they're gaining by restricting it, but presumably they have some reason.
You need to decorate your controller with [HttpOptions]as well as [HttpPost]. Otherwise when it makes a request using the OPTIONS verb, it will throw a 404. So your controller would be
[HttpPost]
[HttpOptions]
[ActionName("Adduser")]
public int Post(UserModel source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
Db.Users.Add(source);
Db.SaveChanges();
return source.UserId;
}
Related
So I have written a simple Azure Function (AF) that accepts (via Http Post method) an IFormCollection, loops through the file collection, pushes each file into an Azure Blob storage container and returns the url to each file.
The function itself works perfectly when I do a single file or multiple file post through Postman using the 'multipart/form-data' header. However when I try to post a file through an xUnit test, I get the following error:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
I have searched high and low for a solution, tried different things, namely;
Replicating the request object to be as close as possible to Postmans request.
Playing around with the 'boundary' in the header.
Setting 'RequestFormLimits' on the function.
None of these have helped so far.
The details are the project are as follows:
Azure Function v3: targeting .netcoreapp3.1
Startup.cs
public class Startup : FunctionsStartup
{
public IConfiguration Configuration { get; private set; }
public override void Configure(IFunctionsHostBuilder builder)
{
var x = builder;
InitializeConfiguration(builder);
builder.Services.AddSingleton(Configuration.Get<UploadImagesAppSettings>());
builder.Services.AddLogging();
builder.Services.AddSingleton<IBlobService,BlobService>();
}
private void InitializeConfiguration(IFunctionsHostBuilder builder)
{
var executionContextOptions = builder
.Services
.BuildServiceProvider()
.GetService<IOptions<ExecutionContextOptions>>()
.Value;
Configuration = new ConfigurationBuilder()
.SetBasePath(executionContextOptions.AppDirectory)
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
}
UploadImages.cs
public class UploadImages
{
private readonly IBlobService BlobService;
public UploadImages(IBlobService blobService)
{
BlobService = blobService;
}
[FunctionName("UploadImages")]
[RequestFormLimits(ValueLengthLimit = int.MaxValue,
MultipartBodyLengthLimit = 60000000, ValueCountLimit = 10)]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
{
List<Uri> returnUris = new List<Uri>();
if (req.ContentLength == 0)
{
string badResponseMessage = $"Request has no content";
return new BadRequestObjectResult(badResponseMessage);
}
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0)
{
foreach (var file in req.Form.Files)
{
if (!file.IsValidImage())
{
string badResponseMessage = $"{file.FileName} is not a valid/accepted Image file";
return new BadRequestObjectResult(badResponseMessage);
}
var uri = await BlobService.CreateBlobAsync(file);
if (uri == null)
{
return new ObjectResult($"Could not blob the file {file.FileName}.");
}
returnUris.Add(uri);
}
}
if (!returnUris.Any())
{
return new NoContentResult();
}
return new OkObjectResult(returnUris);
}
}
Exception Thrown:
The below exception is thrown at the second if statement above, when it tries to process req.Form.Files.Count > 0, i.e.
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) {}
Message:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
Stack Trace:
MultipartReaderStream.UpdatePosition(Int32 read)
MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool`1 bytePool, Nullable`1 limit, CancellationToken cancellationToken)
MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken)
FormFeature.InnerReadFormAsync(CancellationToken cancellationToken)
FormFeature.ReadForm()
DefaultHttpRequest.get_Form()
UploadImages.Run(HttpRequest req) line 42
UploadImagesTests.HttpTrigger_ShouldReturnListOfUploadedUris(String fileNames)
xUnit Test Project: targeting .netcoreapp3.1
Over to the xUnit Test project, basically I am trying to write an integration test. The project references the AF project and has the following classes:
TestHost.cs
public class TestHost
{
public TestHost()
{
var startup = new TestStartup();
var host = new HostBuilder()
.ConfigureWebJobs(startup.Configure)
.ConfigureServices(ReplaceTestOverrides)
.Build();
ServiceProvider = host.Services;
}
public IServiceProvider ServiceProvider { get; }
private void ReplaceTestOverrides(IServiceCollection services)
{
// services.Replace(new ServiceDescriptor(typeof(ServiceToReplace), testImplementation));
}
private class TestStartup : Startup
{
public override void Configure(IFunctionsHostBuilder builder)
{
SetExecutionContextOptions(builder);
base.Configure(builder);
}
private static void SetExecutionContextOptions(IFunctionsHostBuilder builder)
{
builder.Services.Configure<ExecutionContextOptions>(o => o.AppDirectory = Directory.GetCurrentDirectory());
}
}
}
TestCollection.cs
[CollectionDefinition(Name)]
public class TestCollection : ICollectionFixture<TestHost>
{
public const string Name = nameof(TestCollection);
}
HttpRequestFactory.cs: To create Http Post Request
public static class HttpRequestFactory
{
public static DefaultHttpRequest Create(string method, string contentType, Stream body)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var contentTypeWithBoundary = new MediaTypeHeaderValue(contentType)
{
Boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}"
};
var boundary = MultipartRequestHelper.GetBoundary(
contentTypeWithBoundary, (int)body.Length);
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.ContentLength = body.Length;
request.Body = body;
return request;
}
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
if (string.IsNullOrWhiteSpace(boundary.Value))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary.Value;
}
}
The MultipartRequestHelper.cs class is available here
And Finally the Test class:
[Collection(TestCollection.Name)]
public class UploadImagesTests
{
readonly UploadImages UploadImagesFunction;
public UploadImagesTests(TestHost testHost)
{
UploadImagesFunction = new UploadImages(testHost.ServiceProvider.GetRequiredService<IBlobService>());
}
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
var response = (OkObjectResult)await UploadImagesFunction.Run(request);
//fileStream.Close();
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
private static IFormFile GetFormFile(string fileName)
{
string fileExtension = fileName.Substring(fileName.IndexOf('.') + 1);
string fileNameandPath = GetFilePathWithName(fileName);
IFormFile formFile;
var stream = File.OpenRead(fileNameandPath);
switch (fileExtension)
{
case "jpg":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/jpeg"
};
break;
case "png":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/png"
};
break;
case "pdf":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "application/pdf"
};
break;
default:
formFile = null;
break;
}
return formFile;
}
private static string GetFilePathWithName(string filename)
{
var outputFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
return $"{outputFolder.Substring(0, outputFolder.IndexOf("bin"))}testfiles\\{filename}";
}
}
The test seems to be hitting the function and req.ContentLength does have a value. Considering this, could it have something to do with the way the File Streams are being managed? Perhaps not the right way?
Any inputs on this would be greatly appreciated.
Thanks
UPDATE 1
As per this post, I have also tried setting the ValueLengthLimit and MultipartBodyLengthLimit in the Startup of the Azure Function and/or the Test Project as opposed to attributes on the Azure Function. The exception then changed to:
"The inner stream position has changed unexpectedly"
Following this, I then set the fileStream position in the test project to SeekOrigin.Begin. I started getting the same error:
"Multipart body length limit 16384 exceeded."
It took me a 50km bike ride and a good nights sleep but I finally figured this one out :-).
The Azure function (AF) accepts an HttpRequest object as a parameter with the name of 'req' i.e.
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
The hierarchy of the files object in the HttpRequest object (along with the parameter names) is as follows:
HttpRequest -> req
FormCollection -> Form
FormFileCollection -> Files
This is what the AF accepts and one would access the files collection by using req.Form.Files
In my test case, instead of posting a FormCollection object, I was trying to post a Stream of a file to the Azure Function.
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
As a result of this, req.Form had a Stream value that it could not interpret and the req.Form.Files was raising an exception.
In order to rectify this, I had to do the following:
Revert all changes made as part of UPDATE 1. This means that I removed the 'RequestFormLimits' settings from the Startup file and left them as attributes on the functions Run method.
Instantiate a FormFileCollection object and add the IFormFile to it
Instantiate a FormCollection object using this FormFileCollection as a parameter.
Add the FormCollection to the request object.
To achieve the above, I had to make the following changes in code.
Change 'Create' method in the HttpRequestFactory
public static DefaultHttpRequest Create(string method, string contentType, FormCollection formCollection)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}";
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.Form = formCollection;
return request;
}
Add a private static GetFormFiles() method
I wrote an additional GetFormFiles() method that calls the existing GetFormFile() method, instantiate a FormFileCollection object and add the IFormFile to it. This method in turn returns a FormFileCollection.
private static FormFileCollection GetFormFiles(string fileNames)
{
var formFileCollection = new FormFileCollection();
foreach (var file in fileNames.Split(','))
{
formFileCollection.Add(GetFormFile(file));
}
return formFileCollection;
}
Change the Testmethod
The test method calls the GetFormFiles() to get a FormFileCollection then
instantiates a FormCollection object using this FormFileCollection as a parameter and then passes the FormCollection object as a parameter to the HttpRequest object instead of passing a Stream.
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFiles = GetFormFiles(fileNames);
var formCollection = new FormCollection(null, formFiles);
var request = HttpRequestFactory.Create("POST", "multipart/form-data", formCollection);
var response = (OkObjectResult) await UploadImagesFunction.Run(request);
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
So in the end the issue was not really with the 'RequestFormLimits' but rather with the type of data I was submitting in the POST message.
I hope this answer provides a different perspective to someone that comes across the same error message.
Cheers.
Hi I am working in azure functions and azure signal r. I have front end application with azure ad authentication. I want to send signal r notification to specific user through azure function. Below is my react code.
constructor(props: IMapUpload) {
super(props);
this.fileUploaderRef = React.createRef<FileUploader>();
this.hubConnection = new signalR.HubConnectionBuilder().withUrl("http://localhost:7071/api")
.configureLogging(signalR.LogLevel.Information)
.build();// https://wadevdvlgenseawe02-webapi.azurewebsites.net/MapUpload
this.hubConnection.start().catch((err: string) => console.log(err));
}
componentDidMount() {
this.hubConnection.on("newMessage", (message: string) => {
console.log(message);
//Pass the Map File Url to Site Details
this.props.onMapFileUpload(message);
this.handleProgress(message);
});
const sarId= this.props.sarId;
this.props.sar?.getMapFileDetails(sarId, null, null);
}
Below is my azure functions
[FunctionName("Negotiate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req, IBinder binder)
{
if (req.Headers.ContainsKey("Authorization"))
{
var principal = TryGetPrincipal(req.Headers["Authorization"].ToString());
if (principal != null)
{
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(new SignalRConnectionInfoAttribute
{
HubName = "MapUpload",
UserId = principal.FindFirst(ClaimTypes.NameIdentifier).Value
});
return new OkObjectResult(connectionInfo);
}
}
return new UnauthorizedResult();
}
public static ClaimsPrincipal TryGetPrincipal(string jwtToken)
{
IdentityModelEventSource.ShowPII = true;
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.ValidAudience = "e51c317b-87e7-4cb3-95f0-37cb52b6f873";
// validationParameters.ValidIssuer = _issuer.ToLower();
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(".Kmt.LP_f2D3.E8MY.TXyve.-sgr6770j_"));
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
[FunctionName("Negotiate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req, IBinder binder)
{
if (req.Headers.ContainsKey("Authorization"))
{
var principal = TryGetPrincipal(req.Headers["Authorization"].ToString());
if (principal != null)
{
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(new SignalRConnectionInfoAttribute
{
HubName = "MapUpload",
UserId = principal.FindFirst(ClaimTypes.NameIdentifier).Value
});
return new OkObjectResult(connectionInfo);
}
}
return new UnauthorizedResult();
}
public static ClaimsPrincipal TryGetPrincipal(string jwtToken)
{
IdentityModelEventSource.ShowPII = true;
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.ValidAudience = "myclientid";
// validationParameters.ValidIssuer = _issuer.ToLower();
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes("mysecrete"));
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
[FunctionName("Function1")]
public static Task Run([ServiceBusTrigger("myqueue", Connection = "myconn")]string myQueueItem, ILogger log,
[SignalR(HubName = "MapUpload")] IAsyncCollector<SignalRMessage> signalRMessages)
{
log.LogInformation($"C# ServiceBus queue trigger function processed message: {myQueueItem}");
return signalRMessages.AddAsync(
new SignalRMessage
{
//UserId = "test#mydomain.com",
Target = "newMessage",
Arguments = new[] { myQueueItem }
});
}
If I havelogged in with test#mydomain.com in my front end application then only for test#mydomain.com I want to send message. If I add userid then messages are not getting delivered and If I remove message is getting delivered to everyone. Then I did some research and found I need to add UserId = "{headers.x-ms-client-principal-id}" but after adding this I started getting below error
http://localhost:7071/api/negotiate?negotiateVersion=1 500 (Internal Server Error)
Error: Failed to complete negotiation with the server: Error: Internal Server Error
I am struggling to solve this. Can someone help me what I am doing wrong here? Any help would be greatly appreciated. Thank you
Since you are not using the built-in authentication/authorization feature, the header that you are using is not applicable. Instead, you could extract the upn/email from the JWT token passed and leverage runtime binding to pass this extracted value as the UserId.
With this, you should later be able to send notifications to specific users using the passed UserId.
I have an Azure Function App with Azure Active Directory configured but when I call if from my client I keep getting an Unauthorized response.
I have tried a couple different scenarios but nothing worked. Below is a snippet of the last bit of code that I tried.
///
var #params2 = new NameValueCollection
{
{"grant_type", "client_credentials"},
{"client_id", $"{ClientId}"},
{"client_secret", $"{ClientSecret}"},
{"username", userId},
{"resource", "https://management.azure.com/"}
};
var queryString2 = HttpUtility.ParseQueryString(string.Empty);
queryString2.Add(#params2);
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", ClientId},
{"client_secret", ClientSecret},
{"username", userId}
});
var authorityUri2 = $"{string.Format(CultureInfo.InvariantCulture, AadInstance, Tenant).TrimEnd('/')}/oauth2/token";
//var authorityUri2 = $"https://login.microsoftonline.com/{Tenant}/v2.0/.well-known/openid-configuration";
var authUri2 = String.Format("{0}?{1}", authorityUri2, queryString2);
var client2 = new HttpClient();
var message = client2.PostAsync(authorityUri2, content).Result;
//var message = client2.GetAsync(authorityUri2).Result;
var response = message.Content.ReadAsStringAsync().Result;
dynamic values=null;
try
{
values = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
}
catch
{
values = response;
}
var AuthToken2 = values["access_token"];
client2.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AuthToken2);
HttpResponseMessage response2 = await client2.GetAsync(AppBaseAddress.TrimEnd('/') + "/api/AADIntegration");
if (response.IsSuccessStatusCode)
{
// Read the response and data-bind to the GridView to display To Do items.
string s = await response.Content.ReadAsStringAsync();
log.LogInformation($"Success while getting / api / AADIntegration : {s}");
return (ActionResult)new OkObjectResult(s);
}
else
{
string failureDescription = await response.Content.ReadAsStringAsync();
log.LogInformation($"An error occurred while getting / api / AADIntegration : {response.ReasonPhrase}\n {failureDescription}");
return (ActionResult)new OkObjectResult(failureDescription);
}
Data should returned from the Function App.
For client_credentials grant flow your code seems little different. Here I am giving you exact sample for azure function. Just plug and play :))
Example contains:
How would you get token using client_credentials flow
Getting user list From Azure Active Directory tenant using above
token
Access Token Class:
public class AccessTokenClass
{
public string token_type { get; set; }
public string expires_in { get; set; }
public string resource { get; set; }
public string scope { get; set; }
public string access_token { get; set; }
}
Reference To Add:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;
Azure Function Body:
public static class FunctionGetUserList
{
[FunctionName("FunctionGetUserList")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
try
{
log.LogInformation("C# HTTP trigger function processed a request.");
//Token Request endpoint Just replace yourTennantId/Name
string tokenUrl = $"https://login.microsoftonline.com/yourTennantId/Name.onmicrosoft.com/oauth2/token";
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = "b603c7bead87-Your_client_id-e6921e61f925",
["client_secret"] = "Vxf1SluKbgu4P-Your_client_Secret-F0Nf3wE5oGl/2XDSeZ=",
["resource"] = "https://graph.microsoft.com"
});
dynamic json;
AccessTokenClass results = new AccessTokenClass();
HttpClient client = new HttpClient();
var tokenResponse = await client.SendAsync(tokenRequest);
json = await tokenResponse.Content.ReadAsStringAsync();
results = JsonConvert.DeserializeObject<AccessTokenClass>(json);
var accessToken = results.access_token;
//Create Request To Server
using (HttpClient clientNew = new HttpClient())
{
//Pass Token on header
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//Get Data from API
var requestToAzureEndpoint = await client.GetAsync("https://graph.microsoft.com/v1.0/users");
if (requestToAzureEndpoint.IsSuccessStatusCode)
{
var result_string = await requestToAzureEndpoint.Content.ReadAsStringAsync();
dynamic responseResults = JsonConvert.DeserializeObject<dynamic>(result_string);
return new OkObjectResult(responseResults);
}
else
{
var result_string = await requestToAzureEndpoint.Content.ReadAsStringAsync();
return new OkObjectResult(result_string);
}
}
}
catch (Exception ex)
{
return new OkObjectResult(ex.Message);
}
}
}
Point To Remember
For Azure Active Directory List users access make sure you have following permission:
User.Read.All
Permission Type: Application
You can check here. See the screen shot for better understanding; make sure you have clicked "Grant admin consent for yourTenant" after adding permission.
Note: This is how you can access Azure Active Directory Token using Azure Function after that how to access resource using that token to a specific API endpoint efficiently.
Are you sure you have properly implemented this properly? It looks like a few of your parameters are wrong for the client credential flow. Please double check that you are properly following the client credential flow.
The client credential grant flow is documented here : https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
But for more information on getting this properly working in your function app, please refer to the blog below for more information/help on implementing this.
https://blogs.msdn.microsoft.com/ben/2018/11/07/client-app-calling-azure-function-with-aad/
The value of resource is not correct.
Replace {"resource", "https://management.azure.com/"} with {"resource", $"{ClientId}"}
in my service i have the following request:
[Route(#"/api/adddevice", Verbs = "POST")]
public class AddDeviceRequest : IReturn<AddDeviceResponse>
{
public DTOTargetDevice TargetDevice { get; set; }
}
It uses the following DTO:
public class DTOTargetDevice
{
private string _guid = string.Empty;
public string GUID
{
get { return _guid; }
set { _guid = DTOUtils.GetGUID(value); }
}
public string Type { get; set; }
public string DeviceName { get; set; }
public string[] PropertiesName { get; set; }
public string[] PropertiesValue { get; set; }
}
From a C# client, everything works fine, but when i try to call it, TargetDevice is always null.
This is the javascript code i'm using:
var parms = {};
parms.targetDevice = {};
parms.targetDevice.guid=guid();
parms.targetDevice.deviceName = guid();
parms.targetDevice.type = sDeviceType;
parms.targetDevice.propertiesName = (propsName);
parms.targetDevice.propertiesValue = (propsValue);
var config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8;'
}
}
var promise = $http.post("http://" + sIP + ":20001/api/adddevice", parms, config).then(function (response) {
var result = response.data.result;
console.log("Added Device: " + result);
return result;
});
This is the json, taken from Chrom Developer window:
{"targetDevice":{"guid":"e7461703-1b4b-7cd3-6263-3ac0935fb6f7","deviceName":"11e08b8f-d030-8f69-a469-4a1300aa49bf","type":"HTTPDevice","propertiesName":["Name","Acronym"],"propertiesValue":["Device","Device"]}}:
I've tried several options, but i'm always receiveng a null exception on TargetDevice within the request.
I've already read in other threads that simple objects is preferred, but in other services i have even more complex DTO.
Do you have any suggestion about that?
Thanks
Leo
I am quoting you:
This is the json, taken from Chrom Developer window:
The important word from your sentence is this: json. So make sure you specify the correct content type when making your request. You need to be consistent in what you claim to be sending and what you are actually sending to the server:
var config = {
headers: {
'Content-Type': 'application/json'
}
};
In your example you were claiming that you were sending application/x-www-form-urlencoded;charset=utf-8; and then dumping a JSON string, how do you expect the server to be able to cope with this confusion?
I guess that Angular or whatever this $http.post function that you are using will automatically JSON.stringify the parms object before sending it to the server. Maybe it will even automatically add the application/json content type request header, so you could completely get rid of this config variable of yours in this case.
Solved.
Instead of using in SetConfig()
GlobalResponseHeaders =
{
{ "Access-Control-Allow-Origin", "*" },
{ "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" },
{ "Access-Control-Allow-Headers", "Content-Type" },
},
I've used:
Plugins.Add(new CorsFeature());
Thanks guys!
LEo
How do I configure ServiceStack to serve specific error pages (404, 500, etc.) depending on the type of error being returned?
Currently, I'm using the RawHttpHandler below code to ensure that a request for a HTML file is authenticated. However, if the user specifies a non-existent file or endpoint, how can I have it return my 404.html page.
this.RawHttpHandlers.Add(httpReq =>
{
var session = httpReq.GetSession();
if(!session.IsAuthenticated) {
var isHtmlFileRequest = httpReq.PathInfo.EndsWith(".html");
if(isHtmlFileRequest && !files.Any(s => httpReq.PathInfo.ToLower().Contains(s))) {
return new RedirectHttpHandler {
AbsoluteUrl = "/Login.html"
};
}
}
return null;
});
The Error Handling wiki shows different ways to Customize Handling of Exceptions in ServiceStack, e.g you can redirect 404 errors to /404.cshtml with:
public override void Configure(Container container)
{
this.CustomHttpHandlers[HttpStatusCode.NotFound] =
new RazorHandler("/404");
}
CustomHttpHandlers can be any IServiceStackHandler which is just a HttpHandler that supports both ASP.NET and HttpListener requests. The easiest way to create one is to just inherit from IServiceStackHandler. Here's an example of a Custom Static File Handler similar to StaticFileHandler except it only writes the specified filePath instead of using the HTTP Request path:
public class CustomStaticFileHandler : HttpAsyncTaskHandler
{
string filePath;
public CustomStaticFileHandler(string filePath)
{
this.filePath = filePath;
}
public override void ProcessRequest(HttpContextBase context)
{
var httpReq = context.ToRequest(GetType().GetOperationName());
ProcessRequest(httpReq, httpReq.Response, httpReq.OperationName);
}
public override void ProcessRequest(IRequest request, IResponse response,
string operationName)
{
response.EndHttpHandlerRequest(skipClose: true, afterHeaders: r =>
{
var file = HostContext.VirtualPathProvider.GetFile(filePath);
if (file == null)
throw new HttpException(404, "Not Found");
r.SetContentLength(file.Length);
var outputStream = r.OutputStream;
using (var fs = file.OpenRead())
{
fs.CopyTo(outputStream, BufferSize);
outputStream.Flush();
}
}
}
}
This can then be registered as normal, i.e:
public override void Configure(Container container)
{
this.CustomHttpHandlers[HttpStatusCode.NotFound] =
new CustomStaticFileHandler("/404.html");
}