We are using http client factory and delegating handler to invoke a partner api and process the response. AuthenticationDelegating handler as shown below has a responsibility to get the required token for partner api authentication and provide it part of the api request.
Question - Should i register AuthenticationDelegationHandler as Transient or Singleton if the token expiration is set to 24 hours?
startup.cs
services.AddTransient<AuthenticationDelegatingHandler>();
var apiSettings = Configuration.GetSection(nameof(APIParameters)).Get<APIParameters>();
var apRegistrationParameters = Configuration.GetSection(nameof(AppRegistrationParameters)).Get<AppRegistrationParameters>();
services.AddHttpClient("ApiSecuredClient", client =>
{
client.BaseAddress = new Uri(ApiSettings.BaseUrl);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApiSettings.MediaType));
})
.AddHttpMessageHandler<AuthenticationDelegatingHandler>().SetHandlerLifetime(TimeSpan.FromMinutes(apRegistrationParameters.LifetimeOfAuthenticationHandlerInMinutes));
httpclient factory
public virtual async Task<HttpResponseMessage> GetRequestAsync<T>(T req, Sales360APIParameters
_paramenters) where T : class, IApiBaseRequest
{
using (var client = _httpClientFactory.CreateClient("XXXClient"))
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_paramenters.MediaType));
var json = JsonConvert.SerializeObject(req);
var data = new StringContent(json, Encoding.UTF8, _paramenters.MediaType);
data.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", _paramenters.Version));
HttpResponseMessage Res = await client.PostAsync(_paramenters.API, data);
return Res;
}
}
AuthenticationDelegatingHandler
public class AuthenticationDelegatingHandler : DelegatingHandler
{
private AppRegistrationParameters _appRegistrationInfo;
private DateTime _tokenExpirationLocalTime;
private ILogger<AuthenticationDelegatingHandler> _logger;
public AuthenticationDelegatingHandler(ILogger<AuthenticationDelegatingHandler> logger, IConfiguration config)
{
_appRegistrationInfo = config.GetSection(nameof(AppRegistrationParameters)).Get<AppRegistrationParameters>();
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
Task<HttpResponseMessage> response = null;
if (_tokenExpirationLocalTime != null && _tokenExpirationLocalTime > DateTime.Now.AddSeconds(_appRegistrationInfo.TimeoutRequestInSeconds))
{
response = ProcessRequest(request);
if (response.Result.StatusCode == HttpStatusCode.Unauthorized || response.Result.StatusCode == HttpStatusCode.Forbidden)
response = ProcessRequest(request, true);
}
else
{
response = ProcessRequest(request, true);
}
return response.Result;
}
catch (Exception ex)
{
_logger.LogError("{systemId} - Error Authenticating with App. {errorCode} for " + request.Content, Constants.SystemId, Constants.ErrorCode3100);
_logger.LogError(ex.Message);
throw;
}
}
Custom delegating handler must always be registered as transient dependencies. All the examples in the Microsoft documentation show that custom delegating handlers must be registered as transient dependencies.
This stackoverflow question and this article explain the reasons behind this.
To summarize: always register your custom delegating handlers with transient lifetime when you are using the ASP.NET core HTTP client factory.
Notice that this guideline has nothing to do with your business logic to handle both the access token and its lifetime. It is a side effect of the way the ASP.NET core HTTP client factory handles the DI scope which uses to resolve dependencies (read the linked article for an in depth explanation).
If you want to take some ideas about how to handle access tokens and their expiration by using a delegating handler, this project can offer you some insights.
The concept of token expired and HttpDelegationHandler is just quite un-relative.
One that should stick with HttpRequestMessage and the other should stick with HttpClient.
In short, you're trying to interact with the request, response in the exact time that the request was sent, which possibility can get an unauthorize response. That can be done right, but it's quite complicated.
Instead, why not just consider another approach ? Like a background service that run every 30 mins, if your token expire in 3 hour or less, take a new token and store it either on cache or database ? I think that just much more clean and easier than the way you're trying to solve the same problem.
Related
I am setting up a PHP API and a web-page based on client-side Blazor. But for some reason CORS is triggered and my login process or any requests to my PHP pages result in CORS errors.
I started out testing my PHP API with a C# console app and the Blazor app, I tried using without any database access to test the functionality. The Blazor is right now running with Preview 9. The PHP version is 5.3.8. I could in theory update it, but several other active projects are running on it and I do not have any test environment. MySQL version 5.5.24.
First I figured it might have been because I was running it on my local machine, so I pushed it to the website where the PHP and MySQL is also running. Still I run into this CORS error.
I am still just testing this, so I have tried setting it to allow any origin. I have not had any experience with CORS before this. Pretty sure I ought to be able to add PHP code in each file I access that should allow CORS, but since it should all be on the same website, I figure CORS should not even be relevant?
PHP Code:
function cors() {
// Allow from any origin
if (isset($_SERVER['HTTP_ORIGIN'])) {
// Decide if the origin in $_SERVER['HTTP_ORIGIN'] is one
// you want to allow, and if so:
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400'); // cache for 1 day
}
// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
// may also be using PUT, PATCH, HEAD etc
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
exit(0);
}
echo "You have CORS!";
}
cors();
C# code using the injected HttpClient:
var resp = await Http.GetStringAsync(link);
The error I get is:
Access to fetch at 'https://titsam.dk/ntbusit/busitapi/requestLoginToken.php' from origin 'https://www.titsam.dk' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
The response I hoped to get was that the link I use return a token for the login as it does for my API.
Is it because its running client side maybe and this triggers CORS? But that does not seem to explain why I cannot make it allow all.
Update:
My C# code in OnInitializedAsync:
link = API_RequestLoginTokenEndPoint;
Http.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", "basic:testuser:testpass");
var requestMessage = new HttpRequestMessage(HttpMethod.Get, link);
requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
credentials = "include"
};
var response = await Http.SendAsync(requestMessage);
var responseStatusCode = response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync();
output = responseBody + " " + responseStatusCode;
Update 2:
It finally works. The C# code I linked is the solution Agua From Mars suggested and it solved the problem to use SendAsync with a HttpRequestMessage and adding the Fetch property include credentials to it. Another alternative was to add this line to the startup:
WebAssemblyHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include;
Then I could keep doing what I did to begin with, using GetStringAsync as it becomes the default.
await Http.GetStringAsync(API_RequestLoginTokenEndPoint);
So all the solutions Agua From Mars suggested worked. But I encountered a browser problem, where it kept the CORS issue in the cache somehow even after it had gotten solved, so it seemed like nothing had changed. Some code changes would show a different result, but I guess the CORS part was kept alive. With Chrome it helped opening a new pane or window. In my Opera browser this was not enough, I had to close all panes with the site open to ensure it would clear the cache and then opening a new window or pane with the site works in Opera as well. I had already in both browsers trying to use ctrl-F5 and Shift-F5 to get them to clear the cache. This did not change anything.
I hope this will help others avoid spending 2-3 days on an issue like this.
update 3.1-preview3
In 3.1-preview3, we cannot use the fetch option per message, the options is global
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
WebAssemblyHttpMessageHandler has been removed. The HttpMessageHanlder used is WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler from WebAssembly.Net.Http but don't include WebAssembly.Net.Http in your depencies or the application will failled to launch.
If you want to use the HttpClientFactory you can implement like that :
public class CustomDelegationHandler : DelegatingHandler
{
private readonly IUserStore _userStore;
private readonly HttpMessageHandler _innerHanler;
private readonly MethodInfo _method;
public CustomDelegationHandler(IUserStore userStore, HttpMessageHandler innerHanler)
{
_userStore = userStore ?? throw new ArgumentNullException(nameof(userStore));
_innerHanler = innerHanler ?? throw new ArgumentNullException(nameof(innerHanler));
var type = innerHanler.GetType();
_method = type.GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod) ?? throw new InvalidOperationException("Cannot get SendAsync method");
WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue(_userStore.AuthenticationScheme, _userStore.AccessToken);
return _method.Invoke(_innerHanler, new object[] { request, cancellationToken }) as Task<HttpResponseMessage>;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient(p =>
{
var wasmHttpMessageHandlerType = Assembly.Load("WebAssembly.Net.Http")
.GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
var constructor = wasmHttpMessageHandlerType.GetConstructor(Array.Empty<Type>());
return constructor.Invoke(Array.Empty<object>()) as HttpMessageHandler;
})
.AddTransient<CustomDelegationHandler>()
.AddHttpClient("MyApiHttpClientName")
.AddHttpMessageHandler<CustonDelegationHandler>();
}
3.0 -> 3.1-preview2
On Blazor client side your need to tell to the Fetch API to send credentials (cookies and authorization header).
It's describe in the Blazor doc Cross-origin resource sharing (CORS)
requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
credentials = FetchCredentialsOption.Include
};
ex:
#using System.Net.Http
#using System.Net.Http.Headers
#inject HttpClient Http
#code {
private async Task PostRequest()
{
Http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "{OAUTH TOKEN}");
var requestMessage = new HttpRequestMessage()
{
Method = new HttpMethod("POST"),
RequestUri = new Uri("https://localhost:10000/api/TodoItems"),
Content =
new StringContent(
#"{""name"":""A New Todo Item"",""isComplete"":false}")
};
requestMessage.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue(
"application/json");
requestMessage.Content.Headers.TryAddWithoutValidation(
"x-custom-header", "value");
requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
credentials = FetchCredentialsOption.Include
};
var response = await Http.SendAsync(requestMessage);
var responseStatusCode = response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync();
}
}
You can set up this option globaly with WebAssemblyHttpMessageHandlerOptions.DefaultCredentials static proprerty.
Or you can implement a DelegatingHandler and set it up in DI with the HttpClientFactory:
public class CustomWebAssemblyHttpMessageHandler : WebAssemblyHttpMessageHandler
{
internal new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken);
}
}
public class CustomDelegationHandler : DelegatingHandler
{
private readonly CustomWebAssemblyHttpMessageHandler _innerHandler;
public CustomDelegationHandler(CustomWebAssemblyHttpMessageHandler innerHandler)
{
_innerHandler = innerHandler ?? throw new ArgumentNullException(nameof(innerHandler));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
credentials = "include"
};
return _innerHandler.SendAsync(request, cancellationToken);
}
}
In Setup.ConfigureServices
services.AddTransient<CustomWebAssemblyHttpMessageHandler>()
.AddTransient<WebAssemblyHttpMessageHandler>()
.AddTransient<CustomDelegationHandler>()
.AddHttpClient(httpClientName)
.AddHttpMessageHandler<CustomDelegationHandler>();
Then you can create an HttpClient for your API with IHttpClientFactory.CreateClient(httpClientName)
To use the IHttpClientFactory you need to install Microsoft.Extensions.Http package.
3.0-preview3 => 3.0-preview9
Replace WebAssemblyHttpMessageHandler with BlazorHttpMessageHandler
We're using ASP.NET Core 2.2 to build a Web API project that effectively is a smart proxy for another API. We don't want to do anything special with access tokens though, and just forward them when calling the other API.
I want to avoid "sync over async" calls, especially in constructors and DI resolvers and factories, while still relying on the framework to handle retrieving access tokens from the current http request.
Here's a representative repro of our scenario:
public class TheOtherApi : ITheOtherApi
{
private readonly HttpClient _client;
public TheOtherApi(HttpClient client, IHttpContextAccessor httpContextAccessor)
{
_client = client;
// Yuck! Sync over async, inside a constructor!
var token = httpContextAccessor.HttpContext.GetTokenAsync("access_token").Result;
client.SetBearerToken(token);
}
public Task<string> ForwardSomethingCool(MyCommand command)
{
// E.g.
var response await _client.PostAsync(command.path, command.body);
return await response.Content.ReadAsStringAsync();
}
}
Registrations in Startup along these lines:
services.AddScoped<ITheOtherApi, TheOtherApi>();
services.AddHttpContextAccessor();
services.AddScoped(_=> new HttpClient { BaseAddress = new Uri("https://example.org/api") });
This perfectly demonstrates my problem: there's a .Result in there, that I want to get rid of entirely, without just moving it to some kind of factory function registered in Startup.
Searching for a synchronous way to get the access token, I went down into the source of GetTokenAsync(...) and see what other method I could use intead. I find that it in fact has all sorts of side-effects, for example it does AuthenticateAsync(...) before doing what the method name suggests ("getting a token" from the context).
I actually only want the logic from GetTokenValue(...) without the other bits, and hurray (!): it is not async.... but that method relies on AuthenticationProperties which I don't have readily available from the HttpContextAccessor?
My current workaround (or 'solution'?) is to do the work myself:
httpContextAccessor.HttpContext.Request.Headers
.TryGetValue("Authorization", out var authorizationHeader);
var bearerToken = authorizationHeader
.SingleOrDefault()
?.Replace("bearer ", "", StringComparison.InvariantCultureIgnoreCase);
if (string.IsNullOrWhiteSpace(bearerToken))
{
throw new ArgumentException(nameof(httpContextAccessor), "HttpContextAccessor resulted in no Access Token");
}
_client.SetBearerToken(token);
How could I synchronously get the access_token from a IHttpContextAccessor without writing my own entire headers/string manipulation helper function to extract it from the Headers?
I'm developing an ASP.Net Core web application where I need to create a kind of "authentication proxy" to another (external) web service.
What I mean by authentication proxy is that I will receive requests through a specific path of my web app and will have to check the headers of those requests for an authentication token that I'll have issued earlier, and then redirect all the requests with the same request string / content to an external web API which my app will authenticate with through HTTP Basic auth.
Here's the whole process in pseudo-code
Client requests a token by making a POST to a unique URL that I sent him earlier
My app sends him a unique token in response to this POST
Client makes a GET request to a specific URL of my app, say /extapi and adds the auth-token in the HTTP header
My app gets the request, checks that the auth-token is present and valid
My app does the same request to the external web API and authenticates the request using BASIC authentication
My app receives the result from the request and sends it back to the client
Here's what I have for now. It seems to be working fine, but I'm wondering if it's really the way this should be done or if there isn't a more elegant or better solution to this? Could that solution create issues in the long run for scaling the application?
[HttpGet]
public async Task GetStatement()
{
//TODO check for token presence and reject if issue
var queryString = Request.QueryString;
var response = await _httpClient.GetAsync(queryString.Value);
var content = await response.Content.ReadAsStringAsync();
Response.StatusCode = (int)response.StatusCode;
Response.ContentType = response.Content.Headers.ContentType.ToString();
Response.ContentLength = response.Content.Headers.ContentLength;
await Response.WriteAsync(content);
}
[HttpPost]
public async Task PostStatement()
{
using (var streamContent = new StreamContent(Request.Body))
{
//TODO check for token presence and reject if issue
var response = await _httpClient.PostAsync(string.Empty, streamContent);
var content = await response.Content.ReadAsStringAsync();
Response.StatusCode = (int)response.StatusCode;
Response.ContentType = response.Content.Headers.ContentType?.ToString();
Response.ContentLength = response.Content.Headers.ContentLength;
await Response.WriteAsync(content);
}
}
_httpClient being a HttpClient class instantiated somewhere else and being a singleton and with a BaseAddressof http://someexternalapp.com/api/
Also, is there a simpler approach for the token creation / token check than doing it manually?
If anyone is interested, I took the Microsoft.AspNetCore.Proxy code and made it a little better with middleware.
Check it out here: https://github.com/twitchax/AspNetCore.Proxy. NuGet here: https://www.nuget.org/packages/AspNetCore.Proxy/. Microsoft archived the other one mentioned in this post, and I plan on responding to any issues on this project.
Basically, it makes reverse proxying another web server a lot easier by allowing you to use attributes on methods that take a route with args and compute the proxied address.
[ProxyRoute("api/searchgoogle/{query}")]
public static Task<string> SearchGoogleProxy(string query)
{
// Get the proxied address.
return Task.FromResult($"https://www.google.com/search?q={query}");
}
I ended up implementing a proxy middleware inspired by a project in Asp.Net's GitHub.
It basically implements a middleware that reads the request received, creates a copy from it and sends it back to a configured service, reads the response from the service and sends it back to the caller.
This post talks about writing a simple HTTP proxy logic in C# or ASP.NET Core. And allowing your project to proxy the request to any other URL. It is not about deploying a proxy server for your ASP.NET Core project.
Add the following code anywhere of your project.
public static HttpRequestMessage CreateProxyHttpRequest(this HttpContext context, Uri uri)
{
var request = context.Request;
var requestMessage = new HttpRequestMessage();
var requestMethod = request.Method;
if (!HttpMethods.IsGet(requestMethod) &&
!HttpMethods.IsHead(requestMethod) &&
!HttpMethods.IsDelete(requestMethod) &&
!HttpMethods.IsTrace(requestMethod))
{
var streamContent = new StreamContent(request.Body);
requestMessage.Content = streamContent;
}
// Copy the request headers
foreach (var header in request.Headers)
{
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
{
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
requestMessage.Headers.Host = uri.Authority;
requestMessage.RequestUri = uri;
requestMessage.Method = new HttpMethod(request.Method);
return requestMessage;
}
This method covert user sends HttpContext.Request to a reusable HttpRequestMessage. So you can send this message to the target server.
After your target server response, you need to copy the responded HttpResponseMessage to the HttpContext.Response so the user's browser just gets it.
public static async Task CopyProxyHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
{
if (responseMessage == null)
{
throw new ArgumentNullException(nameof(responseMessage));
}
var response = context.Response;
response.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers)
{
response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in responseMessage.Content.Headers)
{
response.Headers[header.Key] = header.Value.ToArray();
}
// SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
response.Headers.Remove("transfer-encoding");
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
{
await responseStream.CopyToAsync(response.Body, _streamCopyBufferSize, context.RequestAborted);
}
}
And now the preparation is complete. Back to our controller:
private readonly HttpClient _client;
public YourController()
{
_client = new HttpClient(new HttpClientHandler()
{
AllowAutoRedirect = false
});
}
public async Task<IActionResult> Rewrite()
{
var request = HttpContext.CreateProxyHttpRequest(new Uri("https://www.google.com"));
var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
await HttpContext.CopyProxyHttpResponse(response);
return new EmptyResult();
}
And try to access it. It will be proxied to google.com
A nice reverse proxy middleware implementation can also be found here: https://auth0.com/blog/building-a-reverse-proxy-in-dot-net-core/
Note that I replaced this line here
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
with
requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToString());
Original headers (e.g. like an authorization header with a bearer token) would not be added without my modification in my case.
I had luck using twitchax's AspNetCore.Proxy NuGet package, but could not get it to work using the ProxyRoute method shown in twitchax's answer. (Could have easily been a mistake on my end.)
Instead I defined the mapping in Statup.cs Configure() method similar to the code below.
app.UseProxy("api/someexternalapp-proxy/{arg1}", async (args) =>
{
string url = "https://someexternalapp.com/" + args["arg1"];
return await Task.FromResult<string>(url);
});
Piggy-backing on James Lawruk's answer https://stackoverflow.com/a/54149906/6596451 to get the twitchax Proxy attribute to work, I was also getting a 404 error until I specified the full route in the ProxyRoute attribute. I had my static route in a separate controller and the relative path from Controller's route was not working.
This worked:
public class ProxyController : Controller
{
[ProxyRoute("api/Proxy/{name}")]
public static Task<string> Get(string name)
{
return Task.FromResult($"http://www.google.com/");
}
}
This does not:
[Route("api/[controller]")]
public class ProxyController : Controller
{
[ProxyRoute("{name}")]
public static Task<string> Get(string name)
{
return Task.FromResult($"http://www.google.com/");
}
}
Hope this helps someone!
Twitchax's answer seems to be the best solution at the moment. In researching this, I found that Microsoft is developing a more robust solution that fits the exact problem the OP was trying to solve.
Repo: https://github.com/microsoft/reverse-proxy
Article for Preview 1 (they actually just released prev 2): https://devblogs.microsoft.com/dotnet/introducing-yarp-preview-1/
From the Article...
YARP is a project to create a reverse proxy server. It started when we noticed a pattern of questions from internal teams at Microsoft who were either building a reverse proxy for their service or had been asking about APIs and technology for building one, so we decided to get them all together to work on a common solution, which has become YARP.
YARP is a reverse proxy toolkit for building fast proxy servers in .NET using the infrastructure from ASP.NET and .NET. The key differentiator for YARP is that it is being designed to be easily customized and tweaked to match the specific needs of each deployment scenario. YARP plugs into the ASP.NET pipeline for handling incoming requests, and then has its own sub-pipeline for performing the steps to proxy the requests to backend servers. Customers can add additional modules, or replace stock modules as needed.
...
YARP works with either .NET Core 3.1 or .NET 5 preview 4 (or later). Download the preview 4 (or greater) of .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0
More specifically, one of their sample apps implements authentication (as for the OP's original intent)
https://github.com/microsoft/reverse-proxy/blob/master/samples/ReverseProxy.Auth.Sample/Startup.cs
Here is a basic implementation of Proxy library for ASP.NET Core:
This does not implement the authorization but could be useful to someone looking for a simple reverse proxy with ASP.NET Core. We only use this for development stages.
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Sample.Proxy
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(options =>
{
options.AddDebug();
options.AddConsole(console =>
{
console.IncludeScopes = true;
});
});
services.AddProxy(options =>
{
options.MessageHandler = new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = true
};
options.PrepareRequest = (originalRequest, message) =>
{
var host = GetHeaderValue(originalRequest, "X-Forwarded-Host") ?? originalRequest.Host.Host;
var port = GetHeaderValue(originalRequest, "X-Forwarded-Port") ?? originalRequest.Host.Port.Value.ToString(CultureInfo.InvariantCulture);
var prefix = GetHeaderValue(originalRequest, "X-Forwarded-Prefix") ?? originalRequest.PathBase;
message.Headers.Add("X-Forwarded-Host", host);
if (!string.IsNullOrWhiteSpace(port)) message.Headers.Add("X-Forwarded-Port", port);
if (!string.IsNullOrWhiteSpace(prefix)) message.Headers.Add("X-Forwarded-Prefix", prefix);
return Task.FromResult(0);
};
});
}
private static string GetHeaderValue(HttpRequest request, string headerName)
{
return request.Headers.TryGetValue(headerName, out StringValues list) ? list.FirstOrDefault() : null;
}
public void Configure(IApplicationBuilder app)
{
app.UseWebSockets()
.Map("/api", api => api.RunProxy(new Uri("http://localhost:8833")))
.Map("/image", api => api.RunProxy(new Uri("http://localhost:8844")))
.Map("/admin", api => api.RunProxy(new Uri("http://localhost:8822")))
.RunProxy(new Uri("http://localhost:8811"));
}
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}
I have a Xamarin based application which uses the Microsoft.OneDriveSDK nuget Package with version 1.x In this application I manage the OAuth stuff using Xamarin.Auth and thus get the access_token from that framework.
With the OneDriveSDK 1.x, I could provide this access token by redefining a few classes and then never had the API trying to fetch the token.
Now I wanted to migrate to version 2 and noticed that the previous classes got replaced and the API now uses Microsoft.Graph nuget package instead. So I had to implement the interface IAuthenticationProvider and did it like this:
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
if (!string.IsNullOrEmpty(MicrosoftLiveOAuthProvider.Instance.AccessToken))
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", MicrosoftLiveOAuthProvider.Instance.AccessToken);
}
}
The code is called and properly provides the authentication token to the request headers. But once the SDK tries to use the token, I get an exception:
Exception of type 'Microsoft.Graph.ServiceException' was thrown.
Code: InvalidAuthenticationToken
Message: CompactToken parsing failed with error code: -2147184118
Now using google for this message always said the token is not JWT compliant and the SDK would then use it as microsoft live account token. But if this is the case, I wonder why it fails with V2 but works with V1.
Authentication is done against:
https://login.live.com/oauth20_authorize.srf
Any help is very mich appreciated!
I use a subclassed Xamarin.Auth WebRedirectAuthenticator with Microsoft.OneDriveSDK v2.0.0.
I get the initial access_token via that Xamarin.Auth subclass using a authorizeUrl: that is built via:
string GetAuthorizeUrl()
{
var requestUriStringBuilder = new StringBuilder();
requestUriStringBuilder.Append(Consts.MicrosoftAccountAuthenticationServiceUrl);
requestUriStringBuilder.AppendFormat("?{0}={1}", Consts.RedirectUriKeyName, Consts.Redirect_URI);
requestUriStringBuilder.AppendFormat("&{0}={1}", Consts.ClientIdKeyName, Consts.Client_ID);
requestUriStringBuilder.AppendFormat("&{0}={1}", Consts.ResponseTypeKeyName, Consts.TokenKeyName);
requestUriStringBuilder.AppendFormat("&{0}={1}", Consts.ScopeKeyName, Consts.Drive_Scopes);
return Uri.EscapeUriString(requestUriStringBuilder.ToString());
}
Once I have the access and refresh tokens, I can implement the IHttpProvider that needs passed to the OneDriveClient constructor in order to set access token in the http header:
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
SetupHttpClient();
return _httpClient.SendAsync(request);
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
SetupHttpClient();
return _httpClient.SendAsync(request, completionOption, cancellationToken);
}
HttpClient _httpClient;
void SetupHttpClient()
{
if (_httpClient == null)
{
_httpClient = new HttpClient();
var accessToken = _account.Properties["access_token"];
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
}
Create your OneDriveClient client using your IAuthenticationProvider and IHttpProvider objects (I just implement them on the same class that I am building all the OneDrive API calls), and every OnDrive request will use the access token from your saved Account.
Note: My IAuthenticationProvider implementation of AuthenticateRequestAsync currently does nothing, but you could do your Account setup here for a cleaner code flow.
var oneDriveClient = new OneDriveClient("https://api.onedrive.com/v1.0", this, this);
var pictureFolderItem = await oneDriveClient.Drive.Root.ItemWithPath("Pictures").Request().GetAsync();
Console.WriteLine(pictureFolderItem.Folder);
Refreshing is almost as easy, I store when the access token will expire (minus 5 minutes) and setup a timer to refresh it and re-save it to the Account. Do the same thing on app launch, if the user has an Account available and thus previously logged in, check if it is expired, refresh it, setup the background timer...
async Task<bool> GetRefreshToken(Account account)
{
// https://github.com/OneDrive/onedrive-api-docs/blob/master/auth/msa_oauth.md#step-3-get-a-new-access-token-or-refresh-token
OneDriveOAuth2Authenticator auth = OAuth2Authenticator();
var token = account.Properties["refresh_token"];
var expiresIn = await auth.RequestRefreshTokenAsync(token);
ResetRefreshTokenTimer(expiresIn);
return true;
}
Over the last few days I've been playing with the micro service pattern and all is going well but security seems to baffle me.
So If I may ask a question:
How do I handle user authentication on an individual service? At the moment I pass a request to the Gateway API which in turns connects to the service.
Question Edited Please See Below
Bearing in mind that the individual services should not know about each other. The Gateway is the aggregator as such.
Current architecture.
A little code to simulate the request:
Frontend - Client App
public class EntityRepository<T>
{
private IGateway _gateway = null;
public EntityRepository(IGateway gateway)
{
this._gateway = gateway;
}
public IEnumerable<T> FindAll()
{
return this._gateway.Get(typeof(T)).Content.ReadAsAsync<IEnumerable<T>>().Result;
}
public T FindById(int id)
{
return this._gateway.Get(typeof(T)).Content.ReadAsAsync<T>().Result;
}
public void Add(T obj)
{
this._gateway.Post(typeof(T), obj);
}
public void Update(T obj)
{
this._gateway.Post(typeof(T), obj);
}
public void Save(T obj)
{
this._gateway.Post(typeof(T), obj);
}
}
//Logic lives elsewhere
public HttpResponseMessage Get(Type type)
{
return Connect().GetAsync(Path(type)).Result;
}
public HttpResponseMessage Post(Type type, dynamic obj)
{
return Connect().PostAsync(Path(type), obj);
}
private string Path(Type type)
{
var className = type.Name;
return "api/service/" + Application.Key + "/" + className;
}
private HttpClient Connect()
{
var client = new HttpClient();
client.BaseAddress = new Uri("X");
// Add an Accept header for JSON format.
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
I use generics to determine where it needs to fire once it hit's the gateway.
So if the Type is Category it will fire the Category service thus calling:
public IEnumerable<dynamic> FindAll(string appKey, string cls)
{
var response = ConnectTo.Service(appKey, cls);
return (appKey == Application.Key) ? (response.IsSuccessStatusCode) ? response.Content.ReadAsAsync<IEnumerable<dynamic>>().Result : null : null;
}
The Gateway does not contain the physical files/Class's of the types.
After a little code, I was hoping someone could give me a little demonstration or the best approach to handle security/user authentication with the current architecture.
Case Scenario 1
User hits the web app and logs in, at that point the users encrypted email and password is sent to the Gateway API which is then passed to the User Service and decides whether the user is authenticated - all well and good but now I want to fetch all Messages from the Message Service that the user has received. I cannot really say in the Gateway if the user is authenticated, fetch the messages because that does not solve the issue of calling the Message Service outside of the Gateway API
I also cannot add authentication to each individual service because that would require all respective services talking to the User Service and that defeats the purpose of the pattern.
Fixes:
Only allow the Gateway to call the Services. Requests to services outside of the Gateway should be blocked.
I know security is a broad topic but within the current context, I'm hoping someone could direct me with the best course of action to resolve the issue.
Currently I have Hardcoded a Guid in all off the applications, which in turn fetches data if the app is equal.
Edit
This answer is about the Gateway <-> Micro service communication. The user should of course be properly authenticated when the App talks with the gateway
end edit
First of all, the micro services should not be reachable from internet. They should only be accessible from the gateway (which can be clustered).
Second, you do need to be able to identify the current user. You can do it by passing the UserId as a HTTP header. Create a WebApi filter which takes that header and creates a custom IPrincipal from it.
Finally you need some way to make sure that the request comes from the gateway or another micro service. An easy way to do that is to use HMAC authentication on a token.
Store the key in the web.config for each service and the gateway. Then just send a token with each request (which you can authenticate using a WebApi authentication filter)
To generate a hash, use the HMACSHA256 class in .NET:
private static string CreateToken(string message, string secret)
{
secret = secret ?? "";
var keyByte = Encoding.ASCII.GetBytes(secret);
var messageBytes = Encoding.ASCII.GetBytes(message);
using (var hasher = new HMACSHA256(keyByte))
{
var hashmessage = hasher.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
So in your MicroServiceClient you would do something like this:
var hash = CreateToken(userId.ToString(), mySharedSecret);
var myHttpRequest = HttpRequest.Create("yourUrl");
myHttpRequest.AddHeader("UserId", userId);
myHttpRequest.AddHeader("UserIdToken", hash);
//send request..
And in the micro service you create a filter like:
public class TokenAuthenticationFilterAttribute : Attribute, IAuthenticationFilter
{
protected string SharedSecret
{
get { return ConfigurationManager.AppSettings["SharedSecret"]; }
}
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
var userId = context.Request.Headers.GetValues("UserId").FirstOrDefault();
if (userId == null)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var userIdToken = context.Request.Headers.GetValues("UserIdToken").FirstOrDefault();
if (userIdToken == null)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var token = CreateToken(userId, SharedSecret);
if (token != userIdToken)
{
context.ErrorResult = new StatusCodeResult(HttpStatusCode.Forbidden, context.Request);
return;
}
var principal = new GenericPrincipal(new GenericIdentity(userId, "CustomIdentification"),
new[] {"ServiceRole"});
context.Principal = principal;
});
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
}
public bool AllowMultiple
{
get { return false; }
}
private static string CreateToken(string message, string secret)
{
secret = secret ?? "";
var keyByte = Encoding.ASCII.GetBytes(secret);
var messageBytes = Encoding.ASCII.GetBytes(message);
using (var hasher = new HMACSHA256(keyByte))
{
var hashmessage = hasher.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
}
Option 1 (Preferred)
The easy way is the micro services should be behind the gateway, hence you would whitelist services to connect to them, meaning only authorized and trusted parties have access (i.e. the gateway only). Clients shouldn't have direct access to them. The Gateway is your night club bouncer.
Option 2
You can use a JWT or some form of token and share the secret key between the services. I use JWT Authorization Bearer tokens.
The other services don't need to query the user service, they just need to know that the token is valid, then they have authorization to use the API. I get the JWT passed from the client to the gateway and inject it into the request that is sent to the other service behind, just a straight pass through.
The micro service behind needs to have the same JWT consumption as the gateway for authorization but as I mentioned that is just determining a valid token, not querying a valid user.
But this has an issue that once someone is authorized they can jump call upon other users data unless you include something like a claim in the token.
My Thoughts
The part that I found a challenge from Monolithic to Micro Services was that you needed to switch where you place your trust. In Monolithic you control everything you are in charge. The point of Micro Services is that other services are in complete control of their domain. You have to place your trust in that other service to fulfill its obligations and not want to recheck and reauthorize everything at every level beyond what is necessary.