From 8056d49643ea141f64a98f9885f3347ccf514dd7 Mon Sep 17 00:00:00 2001 From: Mateus Viegas Date: Thu, 4 Mar 2021 10:17:19 +0000 Subject: [PATCH] Fix/dashboard auth (#793) * Moved app.UseCapDashboard() later in the pipeline and added new parameter to ChallengeAsync with built-in Authorization and Authentication * Added a sample for using Authenticated dashboards with OpenId --- CAP.sln | 7 ++ .../Controllers/ValuesController.cs | 52 ++++++++++++ .../HttpContextDashboardFilter.cs | 21 +++++ .../Program.cs | 17 ++++ ...ple.RabbitMQ.Postgres.DashboardAuth.csproj | 17 ++++ .../Startup.cs | 84 +++++++++++++++++++ .../appsettings.json | 13 +++ .../CAP.DashboardMiddleware.cs | 11 ++- .../CAP.DashboardOptions.cs | 6 ++ 9 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/Controllers/ValuesController.cs create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/HttpContextDashboardFilter.cs create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/Program.cs create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/Sample.RabbitMQ.Postgres.DashboardAuth.csproj create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/Startup.cs create mode 100644 samples/Sample.RabbitMQ.Postgres.DashboardAuth/appsettings.json diff --git a/CAP.sln b/CAP.sln index 947019f..a1b6644 100644 --- a/CAP.sln +++ b/CAP.sln @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.AmazonSQS.InMemory", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.NATS", "src\DotNetCore.CAP.NATS\DotNetCore.CAP.NATS.csproj", "{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.RabbitMQ.Postgres.DashboardAuth", "samples\Sample.RabbitMQ.Postgres.DashboardAuth\Sample.RabbitMQ.Postgres.DashboardAuth.csproj", "{54F6C206-2A23-4971-AE5A-FC47EB772452}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,10 @@ Global {8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Release|Any CPU.Build.0 = Release|Any CPU + {54F6C206-2A23-4971-AE5A-FC47EB772452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54F6C206-2A23-4971-AE5A-FC47EB772452}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54F6C206-2A23-4971-AE5A-FC47EB772452}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54F6C206-2A23-4971-AE5A-FC47EB772452}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -185,6 +191,7 @@ Global {43475E00-51B7-443D-BC2D-FC21F9D8A0B4} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {B187DD15-092D-4B72-9807-50856607D237} = {3A6B6931-A123-477A-9469-8B468B5385AF} {8B2FD3EA-E72B-4A82-B182-B87EC0C15D07} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} + {54F6C206-2A23-4971-AE5A-FC47EB772452} = {3A6B6931-A123-477A-9469-8B468B5385AF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Controllers/ValuesController.cs b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Controllers/ValuesController.cs new file mode 100644 index 0000000..6caa148 --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Controllers/ValuesController.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using DotNetCore.CAP; +using DotNetCore.CAP.Messages; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Sample.RabbitMQ.Postgres.DashboardAuth.Controllers +{ + [Authorize] + [Route("api/[controller]")] + public class ValuesController : Controller + { + private readonly ICapPublisher _capBus; + private readonly ILogger _logger; + private const string CapGroup = "sample.rabbitmq.postgres.dashboard"; + + public ValuesController(ICapPublisher capPublisher, ILogger logger) + { + _capBus = capPublisher; + _logger = logger; + } + + [Route("publish")] + public async Task Publish() + { + await _capBus.PublishAsync(CapGroup, new Person() + { + Id = 123, + Name = "Bar" + }); + + return Ok(); + } + + [NonAction] + [CapSubscribe(CapGroup)] + public void Subscribe(Person p, [FromCap] CapHeader header) + { + var id = header[Headers.MessageId]; + + _logger.LogInformation($@"{DateTime.Now} Subscriber invoked for message {id}, Info: {p}"); + } + + public class Person + { + public int Id { get; set; } + public string Name { get; set; } + } + } +} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/HttpContextDashboardFilter.cs b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/HttpContextDashboardFilter.cs new file mode 100644 index 0000000..a2df5a2 --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/HttpContextDashboardFilter.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using DotNetCore.CAP.Dashboard; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Sample.RabbitMQ.Postgres.DashboardAuth +{ + public class HttpContextDashboardFilter : IDashboardAuthorizationFilter + { + public async Task AuthorizeAsync(DashboardContext context) + { + var httpContextAccessor = context.RequestServices.GetRequiredService(); + + if (httpContextAccessor is null) + throw new ArgumentException("Configure IHttpContextAccessor as a service on Startup"); + + return httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true; + } + } +} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Program.cs b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Program.cs new file mode 100644 index 0000000..26b2d0c --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Sample.RabbitMQ.Postgres.DashboardAuth +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Sample.RabbitMQ.Postgres.DashboardAuth.csproj b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Sample.RabbitMQ.Postgres.DashboardAuth.csproj new file mode 100644 index 0000000..7f79d80 --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Sample.RabbitMQ.Postgres.DashboardAuth.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + + + + + + + + + + + + + diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Startup.cs b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Startup.cs new file mode 100644 index 0000000..481dcbb --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/Startup.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Sample.RabbitMQ.Postgres.DashboardAuth +{ + public class Startup + { + private readonly IConfiguration _configuration; + + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpContextAccessor(); + + services + .AddAuthorization() + .AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(options => + { + options.Authority = "https://demo.identityserver.io/"; + options.ClientId = "interactive.confidential"; + options.ClientSecret = "secret"; + options.ResponseType = "code"; + options.UsePkce = true; + + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + }); + + services.AddCap(cap => + { + cap.UsePostgreSql(p => + { + p.ConnectionString = _configuration.GetConnectionString("Postgres"); + }); + + /* + * Use the command below to start a rabbitmq instance locally: + * docker run -d --name rabbitmq -p 15672:15672 -p 5672:5672 rabbitmq:management + */ + cap.UseRabbitMQ(r => + { + r.Port = 5672; + r.HostName = "127.0.0.1"; + r.UserName = "guest"; + r.Password = "guest"; + }); + + cap.UseDashboard(d => + { + d.UseChallengeOnAuth = true; + d.Authorization = new[] {new HttpContextDashboardFilter()}; + }); + }); + + services.AddControllers(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.Postgres.DashboardAuth/appsettings.json b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/appsettings.json new file mode 100644 index 0000000..39f73ff --- /dev/null +++ b/samples/Sample.RabbitMQ.Postgres.DashboardAuth/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Postgres": "Server=127.0.0.1;Port=5432;Database=cap;Uid=postgres;Pwd=root;Include Error Detail=true;" + } +} diff --git a/src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs b/src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs index d584b2f..441fcf3 100644 --- a/src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs +++ b/src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs @@ -10,6 +10,7 @@ using DotNetCore.CAP.Dashboard.GatewayProxy; using DotNetCore.CAP.Dashboard.NodeDiscovery; using DotNetCore.CAP.Dashboard.Resources; using DotNetCore.CAP.Persistence; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -74,9 +75,9 @@ namespace DotNetCore.CAP { return app => { - app.UseCapDashboard(); - next(app); + + app.UseCapDashboard(); }; } } @@ -133,6 +134,12 @@ namespace DotNetCore.CAP var isAuthenticated = context.User?.Identity?.IsAuthenticated; + if (_options.UseChallengeOnAuth) + { + await context.ChallengeAsync(); + return; + } + context.Response.StatusCode = isAuthenticated == true ? (int)HttpStatusCode.Forbidden : (int)HttpStatusCode.Unauthorized; diff --git a/src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs b/src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs index e04ab24..aec1f0d 100644 --- a/src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs +++ b/src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs @@ -15,8 +15,14 @@ namespace DotNetCore.CAP PathMatch = "/cap"; Authorization = new[] {new LocalRequestsOnlyAuthorizationFilter()}; StatsPollingInterval = 2000; + UseChallengeOnAuth = false; } + /// + /// Indicates if executes a Challenge for Auth within ASP.NET middlewares + /// + public bool UseChallengeOnAuth { get; set; } + /// /// The path for the Back To Site link. Set to in order to hide the Back To Site link. ///