Dynamic service name with ocelot and consul - consul

I am using Ocelot and API gateway with Consul and the service discovery.
I am registering services in the Consul with dynamic names like : service.name.1234 and service.name.5678
This services are statful and not meant to be scaled at all
Since i am working with Ocelot i would like to be able to route a request to the desired service but since the names are dynamic i would need to use the query string parameter as the service name
Example : http://myapp.com/service/1234
Should be redirected to the container with the name service.name.1234
Is there any way to achive this using both products? or maybe other product?
Thank you

I have been searching for myself for the same solution but found only one comment on GitHub and it helped me a lot
So, you need to create custom middleware that will rewrite Ocelot`s DownstreamRoute:
public static async Task InvokeAsync(HttpContext httpContext, Func<Task> next)
{
var downstreamRoute = httpContext.Items.DownstreamRoute();
var yourServiceName = //get query string parameter from httpContext;
//rewrite any parameter that you want
httpContext.Items.UpsertDownstreamRoute(
new DownstreamRoute(
downstreamRoute.Key,
downstreamRoute.UpstreamPathTemplate,
downstreamRoute.UpstreamHeadersFindAndReplace,
downstreamRoute.DownstreamHeadersFindAndReplace,
downstreamRoute.DownstreamAddresses,
tenantServiceName,
...
));
}
And after that call it in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// some other code
var configuration = new OcelotPipelineConfiguration
{
PreQueryStringBuilderMiddleware = async (ctx, next) =>
{
await RouteContextRetrieverMiddleware.InvokeAsync(ctx, next);
await next.Invoke();
}
};
app.UseOcelot(configuration).GetAwaiter().GetResult();
}

Related

How to use OpenIddict AuthServer and Web API in same project

I've built an OpenIddict OAuth server using the marvelous guide by Robin van der Knaap although I've actually implemented it using Identity instead of cookies.
I'm also trying to run a Web API from the same project because the end customer only wants a single system to call.
At the moment I'm doing all my endpoint testing in postman.
This user info endpoint in the AuthorisationController works fine:
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> Userinfo()
{
var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
return Ok(new
{
Name = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),
Occupation = "Developer",
Age = 43
});
}
But when I try to call this custom Web API controller endpoint (https://<domain.com>/api/Test/):
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return Ok("hello");
}
}
I just get the following error:
System.InvalidOperationException: An identity cannot be extracted from
this request. This generally indicates that the OpenIddict server
stack was asked to validate a token for an endpoint it doesn't manage.
To validate tokens received by custom API endpoints, the OpenIddict
validation handler (e.g
OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or
OpenIddictValidationOwinDefaults.AuthenticationType) must be used
instead. at
OpenIddict.Server.OpenIddictServerHandlers.ValidateAuthenticationDemand.HandleAsync(ProcessAuthenticationContext
context) at
OpenIddict.Server.OpenIddictServerDispatcher.DispatchAsync[TContext](TContext
context) at
OpenIddict.Server.OpenIddictServerDispatcher.DispatchAsync[TContext](TContext
context) at
OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandler.HandleAuthenticateAsync()
at
Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync()
at
Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext
context, String scheme) at
Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator.AuthenticateAsync(AuthorizationPolicy
policy, HttpContext context) at
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext
context) at
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext
context) at
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext
context)
My Program.cs looks like this:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OAuthServer;
using OAuthServer.Data;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(connectionString);
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options
.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
options
.SetTokenEndpointUris("/connect/token")
.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo");
// Encryption and signing of tokens TODO: Replace with x.509 cert
options
.AddEncryptionCertificate(CertificateHelper.LoadCertificateFromKeyVault(builder.Configuration["KeyVault:Name"], builder.Configuration["OAuth:EncryptionCertName"]))
.AddSigningCertificate(CertificateHelper.LoadCertificateFromKeyVault(builder.Configuration["KeyVault:Name"], builder.Configuration["OAuth:EncryptionCertName"]))
/*.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()*/
.DisableAccessTokenEncryption();
// Register scopes (permissions)
options.RegisterScopes("api");
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
})
.AddValidation();
builder.Services.AddAuthentication(options => options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddHostedService<TestData>();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
How can I make [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] work on custom API endpoints?
UPDATE
I have a feeling the Web API bit is a red-herring and the issue is something more fundamental. I tried to add an MVC action in the same AuthorisationController:
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/hello")]
public async Task<IActionResult> Hello()
{
return Ok("hi");
}
But that gives the same error.
I think I should probably just be using [Authorize] without specifying the scheme (which is what I was originally trying to do) but that always gives unauthorised.....
I suspect this article has something in it I need, but it's for an old version of OpenIddict (I'm using 3.1.1) and I can't figure out the current behaviour.
Turns out I was just using the wrong scheme on the authorize attribute. I needed to change:
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
to:
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
And if you want to avoid adding the scheme in the attribute then you need to add this to your service configuration:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
NOTE: This seems to conflict with the information on
The OpenIddict ASP.NET Core server cannot be used as the default scheme handler but it's what worked for me.
Although this won't play nice with Identity sign-in (unless I fiddle with the sign-in call) so I left it out.

PostAsync hanging in Xamarin Forms works on emulator but hangs on actual Mobile phone

I have Xamarin Forms project where I'm trying to POST and GET data to/from a Web API but when I'm making an async/await call, it works on the emulator (not without its original problems!) but when I try it on my actual phone mobile (Samsung S8+), it just hangs indefinitely.
Note that I'm only concentrating on the Android part right now, not iOS, not that the problem should make any difference in either.
This is the code I'm using:
IDataService.cs
Task<TResponse> PostDataAsync<TRequest, TResponse>(string uri, TRequest data)
where TRequest : class
where TResponse : class;
DataService.cs:
public async Task<TResponse> PostDataAsync<TRequest, TResponse>(string
additionalUri, TRequest data)
where TRequest : class
where TResponse : class
{
return await WebClient
.PostData<TRequest, TResponse>
(string.Concat(this.Uri, additionalUri), data);
}
WebClient.cs
using (var client = new HttpClient())
{
var jsonData = JsonConvert.SerializeObject(data);
using (var response = await client.PostAsync(
uri,
new StringContent(jsonData,
Encoding.UTF8,
"application/json" )))
{
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TResponse>(content);
}
}
}
Method 1:
LoginPageViewModel.cs
public DelegateCommand SignInCommand => _signInCommand ??
(this._signInCommand = new DelegateCommand(SignInCommandAction));
private async void SignInCommandAction()
{
try
{
....
var user = await this._dataService
.PostDataAsync<LoginRequestDto,
LoginResponseDto>(#"Accounts/Login", loginRequestDto);
....
}
...
}
Method2:
LoginPageViewModel.cs
public DelegateCommand SignInCommand => _signInCommand ??
(this._signInCommand =
new DelegateCommand(async () => await SignInCommandAction()));
private async Task SignInCommandAction()
{
try
{
....
var user = await this._dataService
.PostDataAsync<LoginRequestDto,
LoginResponseDto>(#"Accounts/Login", loginRequestDto);
....
}
...
}
The PostDataAsync works with both methods when I call my local web API i.e. http://10.0.2.2/MyApp/api/ but both methods still hangs when calling external my web service from web provider i.e. http://myapp-123-site.atempurl.com/api/ which is a temp url for testing purpose.
The same apply to my GetDataAsync which is not demonstrated in question but I just thought I'd mention it.
Based on the above, you would think that my async/await code is correct since it works when calling the local web api but then what's causing it to hang when calling the remote web api.
As mentioned, I did enable my INTERNET permission in the manifest.
Any suggestions welcomed?
Thanks.
UPDATE-1:
Note that I've just tried to call a GET opertation within the same function and this is working in the emulator but hanging with the actual mobile.
using (var client = new HttpClient())
{
using (var response = await client.GetAsync(uri))
{
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return Newtonsoft.Json.JsonConvert
.DeserializeObject<TResponse>(content);
}
}
}
UPDATE-2:
This is somehow working and I have no idea why! The only thing that comes to mind is that I upgraded my libraries. This included PRISM which may have been at the source of the problem but I have no idea.
Sorry I can't provide more details. I could role back my code and try to see if it's hanging again but I just don't have the time to go and experiment some more considering the amount of time I've already spent on this. Sorry.
The requested url is an IP or a domain name.
If it is ip, only the IP of the public network can be accessed by devices on multiple network segments.
If it is a domain name, it needs to support the domain name resolution service.
If you do not have these environments for a while, you need the IP of the device and the IP of the server on the same network segment.
The PostDataAsync works with both methods when I call my local web API i.e. http://10.0.2.2/MyApp/api/ but both methods still hangs when calling external my web service from web provider i.e. http://myapp-123-site.atempurl.com/api/ which is a temp url for testing purpose.
From this phenomenon , the reason should be the temp url. From this domain name (myapp-123-site.atempurl.com) can not find the right local IP (10.0.2.2).And when you test in local network , I guess this will work.However the network of actual mobile can be not the same with local network , such as using 3G/4G network , then this will not working.

Identify user/pc without authentication in ASP.NET Core

I'm trying to achieve the following:
Have an unauthenticated user navigate to a web page, where a SignalR (core) client will connect to a hub (say Notifications hub).
Have the user perform an action and, when the operation is completed on the server, use SignalR to notify him of the completion.
The problem: when a user is logged, I find his SignalR connectionId by a connectionId-username map that is saved in memory. Then I do:
hub.SendConnectionAsync(connectionId, "Message", data);
If the user is not authenticated, I came up with using SessionId, and the map I save in memory is something that gives me a ConnectionId given a SessionId. The code snippet I use on the HubLifetimeManager is something like:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId;
if (connection.User.Identity.IsAuthenticated)
{
userId = connection.User.Identity.Name;
}
else
{
var httpContext = connection.GetHttpContext();
if (httpContext == null)
{
throw new Exception("HttpContext can't be null in a SignalR Hub!!");
}
var sessionId = httpContext.Session.Id;
userId = $"{Constants.AnonymousUserIdentifierPrefix}{sessionId}";
}
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Problem: if my page is opened in an iframe, httpContext.Session.Id is the empty string, it looks like the cookies of my page opened in the iframe (among which is the Session cookie), are not added to the http requests performed by the javascript code executed inside the iframe...
More generally, how do you identify a user if he's not authenticated? Is there anything in the HttpRequest that you can use as a unique id, like machine name or ip?
If you want to identify an anonymous user you could use a custom http header generated on frontend. It can be accessed with IHttpContextAccessor in combination with custom IUserIdProvider:
public class CustomUserIdProvider : IUserIdProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetUserId(HubConnectionContext connection)
{
if (connection.User.Identity.IsAuthenticated)
{
return connection.User.Identity.Name;
}
var username = _httpContextAccessor.HttpContext?.Request.Headers["username"];
if (username.HasValue && !StringValues.IsNullOrEmpty(username.Value))
{
return username.Value;
}
return Guid.NewGuid().ToString();
}
}
Remember that in .NET Core you need to explicitly add IHttpContextAccessor to the DI container:
services.AddHttpContextAccessor();
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddSignalR();
Then you can use the generated identifier in hub method like this:
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await _wrappedHubLifetimeManager.OnConnectedAsync(connection);
_connections.Add(connection);
string userId = connection.UserIdentifier;
await _userTracker.AddUser(connection, new UserDetails(connection.ConnectionId, userId));
}
Source: https://dejanstojanovic.net/aspnet/2020/march/custom-signalr-hub-authorization-in-aspnet-core/

Access TempData within custom middleware

I have custom middleware that provides global error handling. If an exception is caught it should log the details with a reference number. I then want to redirect the user to an error page and only show the reference number. My research shows that TempData should be ideal for this but it only seems to be accessible from within a controller context. I tried adding the reference number to HttpContext.Items["ReferenceNumber"] = Guid.NewGuid();
But this value is lost through the redirect.
How can middleware pass information through a redirect? Do I just have to put the number in a querystring?
Inside the middleware class you need to add a reference to get access to the required interfaces. I have this middleware in a separate project and needed to add this NuGet package.
using Microsoft.AspNetCore.Mvc.ViewFeatures;
This then allows you to request the correct services within the middleware.
//get TempData handle
ITempDataDictionaryFactory factory = httpContext.RequestServices.GetService(typeof(ITempDataDictionaryFactory)) as ITempDataDictionaryFactory;
ITempDataDictionary tempData = factory.GetTempData(httpContext);
After you have ITempDataDictionary you can use it like you would use TempData within a controller.
//pass reference number to error controller
Guid ReferenceNum = Guid.NewGuid();
tempData["ReferenceNumber"] = ReferenceNum.ToString();
//log error details
logger.LogError(eventID, exception, ReferenceNum.ToString() + " - " + exception.Message);
Now when I get the the controller after a redirect I have no issues pulling out the reference number and using it in my view.
//read value in controller
string refNum = TempData["ReferenceNumber"] as string;
if (!string.IsNullOrEmpty(refNum))
ViewBag.ReferenceNumber = refNum;
#*display reference number if one was provided*#
#if (ViewBag.ReferenceNumber != null){<p>Reference Number: #ViewBag.ReferenceNumber</p>}
Once you put this all together, you give users a reference number that they can give you to help troubleshoot the problem. But, you are not passing back potentially sensitive error information which could be misused.
You can register an ITempDataProvider yourself and use it in your middleware. Here is a small sample I got working between two simple paths. If you are already using MVC the ITempDataProvider is probably already registered. The issue I faced was the path of the cookie that was written. It was /page1 so /page2 did not have access to the cookie. So I had to override the options as you can see in code below.
I hope this will help you :)
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDataProtectionProvider>(s => DataProtectionProvider.Create("WebApplication2"));
services.Configure<CookieTempDataProviderOptions>(options =>
{
options.Path = "/";
});
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ITempDataProvider tempDataProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Map("/page1", (app1) =>
{
app1.Run(async context =>
{
tempDataProvider.SaveTempData(context, new Dictionary<string, object> { ["Message"] = "Hello from page1 middleware" });
await context.Response.WriteAsync("Hello World! I'm page1");
});
});
app.Map("/page2", (app1) =>
{
app1.Run(async context =>
{
var loadTempData = tempDataProvider.LoadTempData(context);
await context.Response.WriteAsync("Hello World! I'm page2: Message from page1: " + loadTempData["Message"]);
});
});
}
This led me in the right direction: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state#cookie-based-tempdata-provider
Happy coding! :)

How to set up Web API Routing for a Proxy Controller?

Part of my application needs to act as a Proxy Server for a third party RESTful web service. Is there a way to set up Web API routing so that all requests of the same type will go to the same method?
For example, if the client sends in either of these GET requests I want them to go into a single GET action method that then sends on the request to the downstream server.
api/Proxy/Customers/10045
api/Proxy/Customers/10045/orders
api/Proxy/Customers?lastname=smith
The single action method for GET would pick up any one of these three requests and send them on to the respective service (I know how to work with HttpClient to make that happen effectively):
http://otherwebservice.com/Customers/10045
http://otherwebservice.com/Customers/10045/orders
http://otherwebservice.com/Customers?lastname=smith
I don't want to have to tightly couple my web service to the third party web service and replicate their entire API as method calls inside mine.
One workaround that I have thought of is to simply encode the target URL in JavaScript on the client and pass this into the Web API which will then only see one parameter. It would work, but I'd prefer to use the routing capabilities in Web API if possible.
Here's how I got this to work. First, create a controller with a method for each verb you want to support:
public class ProxyController : ApiController
{
private Uri _baseUri = new Uri("http://otherwebservice.com");
public async Task<HttpResponseMessage> Get(string url)
{
}
public async Task<HttpResponseMessage> Post(string url)
{
}
public async Task<HttpResponseMessage> Put(string url)
{
}
public async Task<HttpResponseMessage> Delete(string url)
{
}
}
The methods are async because they're going to use an HttpClient. Map your route like this:
config.Routes.MapHttpRoute(
name: "Proxy",
routeTemplate: "api/Proxy/{*url}",
defaults: new { controller = "Proxy" });
Now back to the Get method in the controller. Create an HttpClient object, create a new HttpRequestMessage object with the appropriate Url, copy everything (or almost everything) from the original request message, then call SendAsync():
public async Task<HttpResponseMessage> Get(string url)
{
using (var httpClient = new HttpClient())
{
string absoluteUrl = _baseUri.ToString() + "/" + url + Request.RequestUri.Query;
var proxyRequest = new HttpRequestMessage(Request.Method, absoluteUrl);
foreach (var header in Request.Headers)
{
proxyRequest.Headers.Add(header.Key, header.Value);
}
return await httpClient.SendAsync(proxyRequest, HttpCompletionOption.ResponseContentRead);
}
}
The URL combining could be more sophisticated, but that's the basic idea.
For the Post and Put methods, you'll also need to copy the request body
Also please note a HttpCompletionOption.ResponseContentRead parameter passed in SendAsync call, because without it, ASP.NET will spend an exremeley long time reading the content if the content is large (in my case, it changed a 500KB 100ms request into a 60s request).

Resources