How can we configure HttpProxy on services.AddAzureClients - azure

we have the following code in our .Net 5.0 aspnetcore startup class
public void ConfigureServices(IServiceCollection services)
{
services.AddAzureClients(builder =>
{
// Add a storage account client
builder.AddBlobServiceClient(storageUrl);
// Use the environment credential by default
builder.UseCredential(new EnvironmentCredential());
});
services.AddControllers();
}
for HttpClient we are able to configure httpproxy using the following code, but how do we acheive the same for BlobServiceClient ??
services.AddHttpClient("SampleClient", client =>
{
client.BaseAddress = new Uri("https://sample.client.url.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
Proxy = new WebProxy("https://sample.proxy.url.com")
});

The solution is to pass BlobClientOptions with the appropriate proxy URI specified, into the constructor of BlobServiceClient.
Checkout this sample code which shows how specify a proxy in a HttpClient
The HttpClient can be passed as a parameter when constructing a new HttpClientTransport, which can be set in the Transport property of BlobClientOption, which can then be passed into the constructor of BlobServiceClient.

Related

Azure AppInsights end to end correlation

I am looking into Azure AppInsights for my telemetry correlation requirement. I have created 3 simple APIs that call one another in succession as below:
First Api -----> Middle Api -----> Another Api
The Api calls are made using Typed HttpClient through a simple service class. All the Api projects have Microsoft.ApplicationInsights.AspNetCore and Microsoft.Extensions.Logging.ApplicationInsights NuGets references added. I have program and service classes for all the APIs as below:
Program.cs
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
...
//App Insights
builder.Services.AddSingleton(typeof(ITelemetryChannel),
new ServerTelemetryChannel() { StorageFolder = "/tmp/myfolder" });
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddScoped<IWeatherService, DummyWeatherService>();
builder.Services.AddHttpClient<IWeatherService, DummyWeatherService>();
var app = builder.Build();
...
app.Run();
Service
using System.Net.Http.Headers;
using AppInsightsDemo.Api.Models;
namespace AppInsightsDemo.Api.Services;
public class DummyWeatherService : IWeatherService
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public DummyWeatherService(
IConfiguration configuration,
HttpClient httpClient)
{
_configuration = configuration;
_httpClient = httpClient;
_httpClient.BaseAddress = GetMiddleApiBaseUri();
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
private Uri GetAnotherApiBaseUri()
{
var configurationSection = _configuration.GetRequiredSection("Dependencies");
var baseUri = configurationSection.GetValue<string>("MiddleApiUri")
?? throw new ArgumentException("Another Api base uri is empty");
return new Uri(baseUri);
}
public async Task<Weather?> GetWeatherAsync()
{
Weather? weather = null;
var response = await _httpClient.GetAsync("middle");
if (response.IsSuccessStatusCode)
{
weather = await response.Content.ReadAsAsync<Weather>();
}
return weather;
}
}
This is what I end up with in AppInsights sample. The third API event has the same operation id as the first two Api events have but the third event has a different parent id. I expect the third event to have the id of my middle (second) api event (localhost://7290) as its parent id and the three events show up accordingly in a hierarchy.
Can anyone please advise if I am missing some configuration or not using this SDK right? Thank you
This is rather silly of me. I configured the ApplicationInsights connection string for my first api(:7176) and last api(:7206) but missed to configure it for my middle api (:7290) though I have added ApplicationInsights service to all Api projects. It took me a while to figure out the missing connection string. Now I get a nice dependency hierarchy as below:
I guess a connection string validation might come handy. Sorry for the trouble. Thanks.

Azure Storage Queue Trigger - Use remote queue name

I am using Azure App Configuration Store to store configuration. I am using the following code in startup.cs to load my config from Azure.
var builder = new ConfigurationBuilder();
builder.AddAzureAppConfiguration(options =>
{
options.Connect(this.Values.AppConfigConnectionString);
options.Select(keyFilter: KeyFilter.Any, labelFilter: this.Values.Env);
});
var config = builder.Build();
Now this config variable contains my queue names. I need this dynamic so to create and handle it in 4 different environments. Dev / Stage / QA / Prod.
public async Task Run(
[QueueTrigger("%QueueName%", Connection = "StorageConnection")]VoiceHubEvent item)
This isn't working as my local.settings.json file doesn't contain QueueName entry.
Is it possible to make use of config variable in Run() to resolve queuename? By reloading queue trigger function or something?
Thanks,
Kiran.
Is it possible to make use of config variable in Run() to resolve queuename? By reloading queue trigger function or something?
Yes, you can.
Create an extensions method for the IWebJobsBuilder interface to set up a connection to AzureAppConfiguration.
public static IWebJobsBuilder AddAzureConfiguration(this IWebJobsBuilder webJobsBuilder)
{
//-- Get current configuration
var configBuilder = new ConfigurationBuilder();
var descriptor = webJobsBuilder.Services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
if (descriptor?.ImplementationInstance is IConfigurationRoot configuration)
configBuilder.AddConfiguration(configuration);
var config = configBuilder.Build();
//-- Add Azure Configuration
configBuilder.AddAzureAppConfiguration(options =>
{
var azureConnectionString = config[TRS.Shared.Constants.CONFIGURATION.KEY_AZURECONFIGURATION_CONNECTIONSTRING];
if (string.IsNullOrWhiteSpace(azureConnectionString)
|| !azureConnectionString.StartsWith("Endpoint=https://"))
throw new InvalidOperationException($"Missing/wrong configuration value for key '{TRS.Shared.Constants.CONFIGURATION.KEY_AZURECONFIGURATION_CONNECTIONSTRING}'.");
options.Connect(azureConnectionString);
});
//build the config again so it has the key vault provider
config = configBuilder.Build();
return webJobsBuilder;
}
Where the azureConnectionString is read from you appsetting.json and should contain the url to the Azure App Configuration.
In startup.cs:
public void Configure(IWebJobsBuilder builder)
{
builder.AddAzureConfiguration();
ConfigureServices(builder.Services)
.BuildServiceProvider(true);
}
For more details, you could refer to this SO thread.

Setting up AAD Authentication in a Project with Autofac

I have a web API that has AAD auth (in code because it runs in IaaS not PaaS) it works well, but if I add Autofac configuration to the Startup.cs, the Authentication breaks, (if I put Autofac after Auth inizialization Autofac breaks) which makes me think that the configurations are overwriting eachother.
I have tried to find any documentation on how to use both of them together but I have not been able to find any information. One Uses HttpConfiguration and the other uses the IAppBuilder and I don't know how to combine them for them to work together.
here is my Authentication code:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.Map("/api", inner =>
{
inner.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions()
{
Tenant = tenant,
TokenValidationParameters = new Tokens.TokenValidationParameters
{
ValidAudience = Audience
}
});
});
}
and here is the Autofac Code
public static void Register(HttpConfiguration configuration)
{
var builder = new ContainerBuilder();
Bootstrapper.Configure(builder);
var container = builder.Build();
configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
what are the best practices for using these two tools together?
I was not properly setting all the WebAPI autofac references to get all the dependencies I followed this Quick start and then Added my references. Bellow is the new ConfigureAutofac function (the configure auth stayed the same)
private void ConfigureAutofac(IAppBuilder app)
{
//Autofac info from https://autofaccn.readthedocs.io/en/latest/integration/webapi.html#quick-start
var builder = new ContainerBuilder();
// STANDARD WEB API SETUP:
// Get your HttpConfiguration. In OWIN, you'll create one
// rather than using GlobalConfiguration.
var config = new HttpConfiguration();
// Register your Web API controllers.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); //Register WebApi Controllers
builder.RegisterType<AutofacManager>().As<IAutofacManager>();
builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver((IContainer)container); //Set the WebApi DependencyResolver
// and finally the standard Web API middleware.
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
}

azure removes Access-Control-Allow-Origin header returned from my app service

I have two services running on Azure :
a web service ( angular app / expressjs )
an app service ( aspnet core app )
All the web service does is query the app service for the following endpoint : my-app-service.azurewebsites.net/.well-known/openid-configuration
My app service is setup to allow CORS requests coming from my web service at the code level via the IdentityServer4 dll and as mentioned in many websites I DID ensure CORS settings were neither overridden by web.config or azure CORS management page.
These are my HTTP request headers :
Accept:application/json, text/plain, */*
Accept-Encoding:gzip, deflate
Host:my-app-service.azurewebsites.net
Origin:http://my-web-service.azurewebsites.net
Pragma:no-cache
Referer:http://my-web-service.azurewebsites.net/
And these are my HTTP response headers
Content-Encoding:gzip
Content-Type:application/json
Date:Fri, 05 Jan 2018 17:22:53 GMT
Server:Kestrel
Set-Cookie:ARRAffinity=da4c4ff244aae03ae3c7548f243f7c2b5c22567a56a76a62aaebc44acc7f0ba8;Path=/;HttpOnly;Domain=Host:my-app-service.azurewebsites.net
Transfer-Encoding:chunked
Vary:Accept-Encoding
X-Powered-By:ASP.NET
As you can see, none of the Access-Control-* headers are present. I have added a custom middleware to the asp.net core app pipeline to trace the response headers and I can clearly see them present.
So somewhere Azure is stripping off my headers and I have no more clues where to look now.
Update #1
I forgot to specify that if everything runs on localhost, it works fine. But it does not on Azure.
Update #2
My identity server 4 code
[...]
using Microsoft.IdentityModel.Tokens;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4;
namespace My.IdentityServer4
{
public class Startup
{
private const string DEFAULT_DEVELOPMENT_AUTHORITY = "http://localhost:5000/";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// [... add db context. identity framework, default token provider]
services.AddMvc();
// Cors ( not required, identity server 4 manages it internally )
//services.AddCors(options =>
// options.AddPolicy("AllowAllOrigins", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
string connectionString = Configuration.GetConnectionString("SQLServer");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
services.AddAuthentication()
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
//TODO: enable HTTPS for production
options.RequireHttpsMetadata = false;
options.Authority = DEFAULT_DEVELOPMENT_AUTHORITY;
options.ClientId = "app"; // implicit
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// [... Some stuff before not useful for this snippet]
// For debug purposes, print out request and response headers
app.UseMiddleware<LogHeadersMiddleware>();
app.UseStaticFiles();
// Cors ( not required, identity server 4 manages it internally )
//app.UseCors("AllowAllOrigins");
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class LogHeadersMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger<LogHeadersMiddleware> logger;
public LogHeadersMiddleware(RequestDelegate next, ILogger<LogHeadersMiddleware> logger)
{
this.next = next;
this.logger = logger;
}
public async Task Invoke(HttpContext context)
{
await this.next.Invoke(context);
logger.LogInformation(
$"------------------------\r\n" +
$"*** Request headers ****\r\n" +
string.Join("\r\n", context.Request.Headers.OrderBy(x => x.Key)) + "\r\n" +
$"*** Response headers ***\r\n" +
string.Join("\r\n", context.Response.Headers.OrderBy(x => x.Key)) + "\r\n" +
$"------------------------\r\n");
}
}
}
Update #3 - CORS on Azure service app is not set
Any hints ? Thanks
#NoName found the answer to my issue on this thread.
In a nutshell, https has to be enabled on Azure in order to work.
A warning from Azure in the logs would have been appreciated though. I wouldn't have lost days on this :S
CORS on Azure service app is not set.
Actually, Azure website is supposed to manage CORS for you. You just need to set the CORS on the Azure service App. I also find a similar SO thread about it.
The good thing is that you can completely disable this middleware and manage CORS by your own means, you just have to remove every single allowed origin (including *) from the CORS settings blade in the portal.

How can I get my custom OWIN auth working in Azure Mobile Service

I have a working Web API 2 mobile service hosted in AWS and I want to move to AMS. It works in Postman and on mobile devices just fine.
I followed several blog/posts and spent several hours rewriting and reordering the WebApiConfig.Register. I then created a new AMS project and copied over all my controllers etc. and I had the same result. I reviewed many similar questions but am brain dead over 20 something lines of code.
It works locally through Postman but after I published it I get
HTTP 401 - {"message":"Authorization has been denied for this request."}
Here is the AWS working startup.cs -- I do not call WebApiConfig.Register
namespace Savviety.Data.Service
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
ConfigureOAuth(app);
// remove in production
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
app.UseWebApi(config);
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var path = AppDomain.CurrentDomain.BaseDirectory + #"\log4net.config";
var fileInfo = new FileInfo(path);
XmlConfigurator.ConfigureAndWatch(fileInfo);
if (fileInfo.Exists)
{
log4net.Config.XmlConfigurator.ConfigureAndWatch(fileInfo);
}
else
{
throw new FileNotFoundException("Could not find log4net.config");
}
}
public void ConfigureOAuth(IAppBuilder app)
{
var oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
}
}
In the AMS version I call the WebApiConfig.Register method from Application.Onstart in Global.asax
public static void Register( )
{
.
var options = new ConfigOptions();
var config = ServiceConfig.Initialize(new ConfigBuilder(options));
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Make sure this is after ServiceConfig.Initialize
// Otherwise ServiceConfig.Initialize will overwrite your changes
Microsoft.WindowsAzure.Mobile.Service.Config.StartupOwinAppBuilder.Initialize(appBuilder =>
{
ConfigureOAuth(appBuilder);
appBuilder.UseWebApi(config);
var path = AppDomain.CurrentDomain.BaseDirectory + #"\log4net.config";
var fileInfo = new FileInfo(path);
});
//var cors = new EnableCorsAttribute("*", "*", "*");
//config.EnableCors(cors);
// Web API routes
// config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
I also replaced [Authorize] with [AuthorizeLevel(AuthorizationLevel.User)] and removed the startup.cs class.
In Postman it works locally, but not after I publish it. It generates a token, but authentication fails.
TIA
Gary
The AuthorizeLevel attribute looks for a token issued by Mobile Services. Since you are not actually issuing such a token in the above, it will fail.
Things are probably working locally since the default config makes all local calls accepted. As described here, you will want to go into the Register() method of WebApiConfig.cs and add the following:
config.SetIsHosted(true);
This should cause calls to start failing locally.
To address the core issue, it is possible to wire your own OWIN provider into the Mobile Services pipeline. You will need to create a child class of LoginProvider which basically does your ConfigureAuth() call inside of its ConfigureMiddleware(). Please see the example in this blog post which sets up a LinkedIn middleware.
Ok, the primary issue is Azure will not support custom OWIN authentication or I cannot find how to implement it anywhere. I have to use a provided list of users and passwords from another system so it has to be custom.
The solution is a custom LoginController and LoginProvider the relevant code is below.
MyLoginProvider is a subclass of LoginProvider and calls the CreateLoginResult base method.
I had to modify my javascript auth interceptor to config.headers["X-ZUMO-AUTH"] = $localStorage.token; instead of the OAuth bearer token header.
I cannot get the email or display name from the claims identity on a request but I used a work around. When I figure it out I will post it here, but for now it is not blocking me.
public HttpResponseMessage Post(LoginRequest loginRequest)
{
var mongoDbManager = MongoDbManager.GetInstance();
var userCollection = mongoDbManager.GetCollection<UserDocument>(CollectionNames.User);
var q0 = Query<UserDocument>.EQ(i => i.ClientId, loginRequest.ClientId);
var q1 = Query<UserDocument>.EQ(i => i.UserEmailAddress, loginRequest.UserName);
var q2 = Query<UserDocument>.EQ(i => i.UserPassword, loginRequest.Password);
var query = Query.And(q0, q1, q2);
var result = userCollection.FindOne(query);
if (result == null)
{
return this.Request.CreateResponse(HttpStatusCode.Unauthorized, "Invalid username or password");
}
else
{
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, result.UserId));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Email, result.UserEmailAddress));
claimsIdentity.AddClaim(new Claim("DisplayName", result.DisplayName));
var loginResult = new SavvietyLoginProvider(handler).CreateLoginResult(claimsIdentity, Services.Settings.MasterKey);
return this.Request.CreateResponse(HttpStatusCode.OK, loginResult);
}
}
}

Resources