Browse Source

Port WebhookClient to WebApplicationBuilder

davidfowl/common-services
Reuben Bond 1 year ago
parent
commit
6f4ae509f1
13 changed files with 143 additions and 196 deletions
  1. +0
    -9
      src/Services/Identity/Identity.API/AppSettings.cs
  2. +3
    -3
      src/Web/WebMVC/Extensions/Extensions.cs
  3. +1
    -1
      src/Web/WebMVC/Program.cs
  4. +0
    -2
      src/Web/WebhookClient/Controllers/AccountController.cs
  5. +5
    -6
      src/Web/WebhookClient/Controllers/WebhooksReceivedController.cs
  6. +49
    -0
      src/Web/WebhookClient/Extensions/Extensions.cs
  7. +3
    -13
      src/Web/WebhookClient/HttpClientAuthorizationDelegatingHandler.cs
  8. +7
    -7
      src/Web/WebhookClient/Pages/RegisterWebhook.cshtml.cs
  9. +69
    -4
      src/Web/WebhookClient/Program.cs
  10. +4
    -5
      src/Web/WebhookClient/Services/WebhooksClient.cs
  11. +0
    -144
      src/Web/WebhookClient/Startup.cs
  12. +1
    -1
      src/Web/WebhookClient/WebhookClientOptions.cs
  13. +1
    -1
      src/docker-compose.override.yml

+ 0
- 9
src/Services/Identity/Identity.API/AppSettings.cs View File

@ -1,9 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Identity.API
{
public class AppSettings
{
public string MvcClient { get; set; }
public bool UseCustomizationData { get; set; }
}
}

+ 3
- 3
src/Web/WebMVC/Extensions/Extensions.cs View File

@ -10,7 +10,7 @@ internal static class Extensions
.AddUrlGroup(_ => new Uri(configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }); .AddUrlGroup(_ => new Uri(configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" });
} }
public static void AddApplicationSevices(this IServiceCollection services, IConfiguration configuration)
public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{ {
services.Configure<AppSettings>(configuration); services.Configure<AppSettings>(configuration);
@ -86,13 +86,13 @@ internal static class Extensions
{ {
// Forward the SignalR traffic to the bff // Forward the SignalR traffic to the bff
var destination = app.Configuration.GetRequiredValue("PurchaseUrl"); var destination = app.Configuration.GetRequiredValue("PurchaseUrl");
var authTransformer = new BffAuthTransfomer();
var authTransformer = new BffAuthTransformer();
var requestConfig = new ForwarderRequestConfig(); var requestConfig = new ForwarderRequestConfig();
return app.MapForwarder("/hub/notificationhub/{**any}", destination, requestConfig, authTransformer); return app.MapForwarder("/hub/notificationhub/{**any}", destination, requestConfig, authTransformer);
} }
private sealed class BffAuthTransfomer : HttpTransformer
private sealed class BffAuthTransformer : HttpTransformer
{ {
public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)
{ {


+ 1
- 1
src/Web/WebMVC/Program.cs View File

@ -6,7 +6,7 @@ builder.Services.AddHttpForwarder();
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddHealthChecks(builder.Configuration); builder.Services.AddHealthChecks(builder.Configuration);
builder.Services.AddApplicationSevices(builder.Configuration);
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddAuthenticationServices(builder.Configuration); builder.Services.AddAuthenticationServices(builder.Configuration);
builder.Services.AddHttpClientServices(); builder.Services.AddHttpClientServices();


+ 0
- 2
src/Web/WebhookClient/Controllers/AccountController.cs View File

@ -5,8 +5,6 @@ public class AccountController : Controller
{ {
public async Task<IActionResult> SignIn(string returnUrl) public async Task<IActionResult> SignIn(string returnUrl)
{ {
var user = User as ClaimsPrincipal;
var token = await HttpContext.GetTokenAsync("access_token"); var token = await HttpContext.GetTokenAsync("access_token");
if (token != null) if (token != null)


+ 5
- 6
src/Web/WebhookClient/Controllers/WebhooksReceivedController.cs View File

@ -4,14 +4,13 @@
[Route("webhook-received")] [Route("webhook-received")]
public class WebhooksReceivedController : Controller public class WebhooksReceivedController : Controller
{ {
private readonly Settings _settings;
private readonly WebhookClientOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IHooksRepository _hooksRepository; private readonly IHooksRepository _hooksRepository;
public WebhooksReceivedController(IOptions<Settings> settings, ILogger<WebhooksReceivedController> logger, IHooksRepository hooksRepository)
public WebhooksReceivedController(IOptions<WebhookClientOptions> options, ILogger<WebhooksReceivedController> logger, IHooksRepository hooksRepository)
{ {
_settings = settings.Value;
_options = options.Value;
_logger = logger; _logger = logger;
_hooksRepository = hooksRepository; _hooksRepository = hooksRepository;
} }
@ -22,9 +21,9 @@ public class WebhooksReceivedController : Controller
var header = Request.Headers[HeaderNames.WebHookCheckHeader]; var header = Request.Headers[HeaderNames.WebHookCheckHeader];
var token = header.FirstOrDefault(); var token = header.FirstOrDefault();
_logger.LogInformation("Received hook with token {Token}. My token is {MyToken}. Token validation is set to {ValidateToken}", token, _settings.Token, _settings.ValidateToken);
_logger.LogInformation("Received hook with token {Token}. My token is {MyToken}. Token validation is set to {ValidateToken}", token, _options.Token, _options.ValidateToken);
if (!_settings.ValidateToken || _settings.Token == token)
if (!_options.ValidateToken || _options.Token == token)
{ {
_logger.LogInformation("Received hook is going to be processed"); _logger.LogInformation("Received hook is going to be processed");
var newHook = new WebHookReceived() var newHook = new WebHookReceived()


+ 49
- 0
src/Web/WebhookClient/Extensions/Extensions.cs View File

@ -0,0 +1,49 @@
namespace WebhookClient;
internal static class Extensions
{
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var identityUrl = configuration.GetValue<string>("IdentityUrl");
var callBackUrl = configuration.GetValue<string>("CallBackUrl");
// Add Authentication services
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(setup => setup.ExpireTimeSpan = TimeSpan.FromHours(2))
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl.ToString();
options.SignedOutRedirectUri = callBackUrl.ToString();
options.ClientId = "webhooksclient";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("webhooks");
});
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
}

+ 3
- 13
src/Web/WebhookClient/HttpClientAuthorizationDelegatingHandler.cs View File

@ -1,7 +1,6 @@
namespace WebhookClient; namespace WebhookClient;
public class HttpClientAuthorizationDelegatingHandler
: DelegatingHandler
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler
{ {
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
@ -12,15 +11,14 @@ public class HttpClientAuthorizationDelegatingHandler
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{ {
var authorizationHeader = _httpContextAccessor.HttpContext
.Request.Headers["Authorization"];
var authorizationHeader = _httpContextAccessor.HttpContext.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authorizationHeader)) if (!string.IsNullOrEmpty(authorizationHeader))
{ {
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
} }
var token = await GetToken();
var token = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
if (token != null) if (token != null)
{ {
@ -29,12 +27,4 @@ public class HttpClientAuthorizationDelegatingHandler
return await base.SendAsync(request, cancellationToken); return await base.SendAsync(request, cancellationToken);
} }
async Task<string> GetToken()
{
const string ACCESS_TOKEN = "access_token";
return await _httpContextAccessor.HttpContext
.GetTokenAsync(ACCESS_TOKEN);
}
} }

+ 7
- 7
src/Web/WebhookClient/Pages/RegisterWebhook.cshtml.cs View File

@ -7,7 +7,7 @@ namespace WebhookClient.Pages
public class RegisterWebhookModel : PageModel public class RegisterWebhookModel : PageModel
{ {
private readonly Settings _settings;
private readonly WebhookClientOptions _options;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
[BindProperty] public string Token { get; set; } [BindProperty] public string Token { get; set; }
@ -18,23 +18,23 @@ namespace WebhookClient.Pages
public string ResponseMessage { get; set; } public string ResponseMessage { get; set; }
public string RequestBodyJson { get; set; } public string RequestBodyJson { get; set; }
public RegisterWebhookModel(IOptions<Settings> settings, IHttpClientFactory httpClientFactory)
public RegisterWebhookModel(IOptions<WebhookClientOptions> options, IHttpClientFactory httpClientFactory)
{ {
_settings = settings.Value;
_options = options.Value;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
} }
public void OnGet() public void OnGet()
{ {
ResponseCode = (int)HttpStatusCode.OK; ResponseCode = (int)HttpStatusCode.OK;
Token = _settings.Token;
Token = _options.Token;
} }
public async Task<IActionResult> OnPost() public async Task<IActionResult> OnPost()
{ {
ResponseCode = (int)HttpStatusCode.OK; ResponseCode = (int)HttpStatusCode.OK;
var protocol = Request.IsHttps ? "https" : "http"; var protocol = Request.IsHttps ? "https" : "http";
var selfurl = !string.IsNullOrEmpty(_settings.SelfUrl) ? _settings.SelfUrl : $"{protocol}://{Request.Host}/{Request.PathBase}";
var selfurl = !string.IsNullOrEmpty(_options.SelfUrl) ? _options.SelfUrl : $"{protocol}://{Request.Host}/{Request.PathBase}";
if (!selfurl.EndsWith("/")) if (!selfurl.EndsWith("/"))
{ {
selfurl = selfurl + "/"; selfurl = selfurl + "/";
@ -50,7 +50,7 @@ namespace WebhookClient.Pages
Url = url, Url = url,
Token = Token Token = Token
}; };
var response = await client.PostAsync<WebhookSubscriptionRequest>(_settings.WebhooksUrl + "/api/v1/webhooks", payload, new JsonMediaTypeFormatter());
var response = await client.PostAsync<WebhookSubscriptionRequest>(_options.WebhooksUrl + "/api/v1/webhooks", payload, new JsonMediaTypeFormatter());
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
@ -68,4 +68,4 @@ namespace WebhookClient.Pages
return Page(); return Page();
} }
} }
}
}

+ 69
- 4
src/Web/WebhookClient/Program.cs View File

@ -1,6 +1,71 @@
CreateWebHostBuilder(args).Build().Run();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSession(opt =>
{
opt.Cookie.Name = ".eShopWebhooks.Session";
})
.Configure<WebhookClientOptions>(builder.Configuration)
.AddHttpClientServices(builder.Configuration)
.AddCustomAuthentication(builder.Configuration)
.AddTransient<IWebhooksClient, WebhooksClient>()
.AddSingleton<IHooksRepository, InMemoryHooksRepository>()
.AddMvc();
builder.Services.AddControllers();
var app = builder.Build();
IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
var pathBase = app.Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
}
app.Map("/check", capp =>
{
capp.Run(async (context) =>
{
if ("OPTIONS".Equals(context.Request.Method, StringComparison.InvariantCultureIgnoreCase))
{
var validateToken = bool.TrueString.Equals(builder.Configuration["ValidateToken"], StringComparison.InvariantCultureIgnoreCase);
var header = context.Request.Headers[HeaderNames.WebHookCheckHeader];
var value = header.FirstOrDefault();
var tokenToValidate = builder.Configuration["Token"];
if (!validateToken || value == tokenToValidate)
{
if (!string.IsNullOrWhiteSpace(tokenToValidate))
{
context.Response.Headers.Add(HeaderNames.WebHookCheckHeader, tokenToValidate);
}
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
else
{
await context.Response.WriteAsync("Invalid token");
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
});
});
// Fix samesite issue when running eShop from docker-compose locally as by default http protocol is being used
// Refer to https://github.com/dotnet-architecture/eShopOnContainers/issues/1391
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax });
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
await app.RunAsync();

+ 4
- 5
src/Web/WebhookClient/Services/WebhooksClient.cs View File

@ -2,18 +2,17 @@
public class WebhooksClient : IWebhooksClient public class WebhooksClient : IWebhooksClient
{ {
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly Settings _settings;
public WebhooksClient(IHttpClientFactory httpClientFactory, IOptions<Settings> settings)
private readonly WebhookClientOptions _options;
public WebhooksClient(IHttpClientFactory httpClientFactory, IOptions<WebhookClientOptions> options)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_settings = settings.Value;
_options = options.Value;
} }
public async Task<IEnumerable<WebhookResponse>> LoadWebhooks() public async Task<IEnumerable<WebhookResponse>> LoadWebhooks()
{ {
var client = _httpClientFactory.CreateClient("GrantClient"); var client = _httpClientFactory.CreateClient("GrantClient");
var response = await client.GetAsync(_settings.WebhooksUrl + "/api/v1/webhooks");
var response = await client.GetAsync(_options.WebhooksUrl + "/api/v1/webhooks");
var json = await response.Content.ReadAsStringAsync(); var json = await response.Content.ReadAsStringAsync();
var subscriptions = JsonSerializer.Deserialize<IEnumerable<WebhookResponse>>(json, new JsonSerializerOptions var subscriptions = JsonSerializer.Deserialize<IEnumerable<WebhookResponse>>(json, new JsonSerializerOptions
{ {


+ 0
- 144
src/Web/WebhookClient/Startup.cs View File

@ -1,144 +0,0 @@
namespace WebhookClient;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSession(opt =>
{
opt.Cookie.Name = ".eShopWebhooks.Session";
})
.AddConfiguration(Configuration)
.AddHttpClientServices(Configuration)
.AddCustomAuthentication(Configuration)
.AddTransient<IWebhooksClient, WebhooksClient>()
.AddSingleton<IHooksRepository, InMemoryHooksRepository>()
.AddMvc();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
if (!env.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
}
app.Map("/check", capp =>
{
capp.Run(async (context) =>
{
if ("OPTIONS".Equals(context.Request.Method, StringComparison.InvariantCultureIgnoreCase))
{
var validateToken = bool.TrueString.Equals(Configuration["ValidateToken"], StringComparison.InvariantCultureIgnoreCase);
var header = context.Request.Headers[HeaderNames.WebHookCheckHeader];
var value = header.FirstOrDefault();
var tokenToValidate = Configuration["Token"];
if (!validateToken || value == tokenToValidate)
{
if (!string.IsNullOrWhiteSpace(tokenToValidate))
{
context.Response.Headers.Add(HeaderNames.WebHookCheckHeader, tokenToValidate);
}
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
else
{
await context.Response.WriteAsync("Invalid token");
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
});
});
// Fix samesite issue when running eShop from docker-compose locally as by default http protocol is being used
// Refer to https://github.com/dotnet-architecture/eShopOnContainers/issues/1391
app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax });
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapRazorPages();
});
}
}
static class ServiceExtensions
{
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<Settings>(configuration);
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var identityUrl = configuration.GetValue<string>("IdentityUrl");
var callBackUrl = configuration.GetValue<string>("CallBackUrl");
// Add Authentication services
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(setup => setup.ExpireTimeSpan = TimeSpan.FromHours(2))
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl.ToString();
options.SignedOutRedirectUri = callBackUrl.ToString();
options.ClientId = "webhooksclient";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("webhooks");
});
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
}

src/Web/WebhookClient/Settings.cs → src/Web/WebhookClient/WebhookClientOptions.cs View File

@ -1,6 +1,6 @@
namespace WebhookClient; namespace WebhookClient;
public class Settings
public class WebhookClientOptions
{ {
public string Token { get; set; } public string Token { get; set; }
public string IdentityUrl { get; set; } public string IdentityUrl { get; set; }

+ 1
- 1
src/docker-compose.override.yml View File

@ -305,7 +305,7 @@ services:
environment: environment:
- ASPNETCORE_URLS=http://0.0.0.0:80 - ASPNETCORE_URLS=http://0.0.0.0:80
- Token=6168DB8D-DC58-4094-AF24-483278923590 # Webhooks are registered with this token (any value is valid) but the client won't check it - Token=6168DB8D-DC58-4094-AF24-483278923590 # Webhooks are registered with this token (any value is valid) but the client won't check it
- Identity__Url=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
- CallBackUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5114 - CallBackUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5114
- WebhooksUrl=http://webhooks-api - WebhooksUrl=http://webhooks-api
- SelfUrl=http://webhooks-client/ - SelfUrl=http://webhooks-client/


Loading…
Cancel
Save