From 74b7c43b5b09ec2b0391903d6a84b01c60cd3190 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 24 Oct 2016 23:09:34 +0200 Subject: [PATCH] Fix --- src/Squidex/Configurations/Constants.cs | 21 +++++ .../Identity/IdentityServices.cs | 42 +++------ .../Configurations/Identity/IdentityUsage.cs | 22 ++--- .../Identity/LazyClientStore.cs | 65 ++++++++++++++ .../Identity/MyIdentityOptions.cs | 10 ++- .../WebModule.cs} | 16 ++-- src/Squidex/Modules/Api/Apps/AppController.cs | 5 +- .../Api/Schemas/SchemaFieldsController.cs | 18 ++-- .../Modules/UI/Account/AccountController.cs | 4 +- src/Squidex/Pipeline/AppFeature.cs | 8 ++ src/Squidex/Pipeline/AppFilterAttribute.cs | 51 +++++++++++ src/Squidex/Pipeline/AppMiddleware.cs | 46 ---------- src/Squidex/Startup.cs | 85 ++++++++++++++++--- .../app/shared/services/auth.service.ts | 10 ++- 14 files changed, 277 insertions(+), 126 deletions(-) create mode 100644 src/Squidex/Configurations/Constants.cs create mode 100644 src/Squidex/Configurations/Identity/LazyClientStore.cs rename src/Squidex/Configurations/{Domain/InfrastructureUsage.cs => Web/WebModule.cs} (55%) create mode 100644 src/Squidex/Pipeline/AppFilterAttribute.cs delete mode 100644 src/Squidex/Pipeline/AppMiddleware.cs diff --git a/src/Squidex/Configurations/Constants.cs b/src/Squidex/Configurations/Constants.cs new file mode 100644 index 000000000..0d42e3808 --- /dev/null +++ b/src/Squidex/Configurations/Constants.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Constants.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Configurations +{ + public class Constants + { + public const string ApiPrefix = "/api"; + + public const string ApiScope = "squidex-api"; + + public const string FrontendClient = "squidex-frontend"; + + public const string IdentityPrefix = "/identity-server"; + } +} diff --git a/src/Squidex/Configurations/Identity/IdentityServices.cs b/src/Squidex/Configurations/Identity/IdentityServices.cs index 4a93071fb..948603232 100644 --- a/src/Squidex/Configurations/Identity/IdentityServices.cs +++ b/src/Squidex/Configurations/Identity/IdentityServices.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography.X509Certificates; using IdentityServer4.Models; +using IdentityServer4.Stores; +using IdentityServer4.Stores.InMemory; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.Extensions.DependencyInjection; @@ -24,10 +26,14 @@ namespace Squidex.Configurations.Identity var certificate = new X509Certificate2(certPath, "password"); - services.AddIdentityServer() - .SetSigningCredential(certificate) - .AddInMemoryScopes(GetScopes()) - .AddInMemoryClients(GetClients()) + services.AddSingleton( + GetScopes()); + services.AddSingleton(); + services.AddSingleton(); + + services.AddIdentityServer().SetSigningCredential(certificate) .AddAspNetIdentity(); return services; @@ -47,35 +53,9 @@ namespace Squidex.Configurations.Identity { StandardScopes.OpenId, StandardScopes.Profile, - new Scope { - Name = "api1", - Description = "My API" - } - }; - } - - public static IEnumerable GetClients() - { - return new List - { - new Client - { - ClientId = "management-portal", - ClientName = "MVC Client", - RedirectUris = new List - { - "http://localhost:5000/account/client-silent", - "http://localhost:5000/account/client-popup" - }, - AllowedGrantTypes = GrantTypes.Implicit, - AllowedScopes = new List - { - StandardScopes.OpenId.Name, - StandardScopes.Profile.Name - }, - RequireConsent = false + Name = Constants.ApiScope, Type = ScopeType.Resource } }; } diff --git a/src/Squidex/Configurations/Identity/IdentityUsage.cs b/src/Squidex/Configurations/Identity/IdentityUsage.cs index 0466f3d81..004a742ab 100644 --- a/src/Squidex/Configurations/Identity/IdentityUsage.cs +++ b/src/Squidex/Configurations/Identity/IdentityUsage.cs @@ -24,17 +24,12 @@ namespace Squidex.Configurations.Identity { app.UseIdentity(); - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = "Cookies" - }); - return app; } public static IApplicationBuilder UseMyIdentityServer(this IApplicationBuilder app) { - app.UseIdentityServer(); + app.UseIdentityServer(); return app; } @@ -85,18 +80,19 @@ namespace Squidex.Configurations.Identity public static IApplicationBuilder UseMyApiProtection(this IApplicationBuilder app) { + const string apiScope = Constants.ApiScope; + var options = app.ApplicationServices.GetService>().Value; if (!string.IsNullOrWhiteSpace(options.BaseUrl)) { - app.Map("/api", api => + var apiAuthorityUrl = options.BuildUrl(Constants.IdentityPrefix); + + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions { - api.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions - { - Authority = options.BaseUrl, - ScopeName = "api", - RequireHttpsMetadata = options.RequiresHttps - }); + Authority = apiAuthorityUrl, + ScopeName = apiScope, + RequireHttpsMetadata = options.RequiresHttps }); } diff --git a/src/Squidex/Configurations/Identity/LazyClientStore.cs b/src/Squidex/Configurations/Identity/LazyClientStore.cs new file mode 100644 index 000000000..6eb729a3e --- /dev/null +++ b/src/Squidex/Configurations/Identity/LazyClientStore.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// LazyClientStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; + +namespace Squidex.Configurations.Identity +{ + public class LazyClientStore : IClientStore + { + private readonly Dictionary clients = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public LazyClientStore(IOptions identityOptions) + { + Guard.NotNull(identityOptions, nameof(identityOptions)); + + foreach (var client in CreateClients(identityOptions.Value)) + { + clients[client.ClientId] = client; + } + } + + public Task FindClientByIdAsync(string clientId) + { + var client = clients.GetOrDefault(clientId); + + return Task.FromResult(client); + } + + private static IEnumerable CreateClients(MyIdentityOptions options) + { + const string id = Constants.FrontendClient; + + yield return new Client + { + ClientId = id, + ClientName = id, + RedirectUris = new List + { + options.BuildUrl("identity-server/client-callback-silent/"), + options.BuildUrl("identity-server/client-callback-popup/") + }, + AllowAccessTokensViaBrowser = true, + AllowedGrantTypes = GrantTypes.Implicit, + AllowedScopes = new List + { + StandardScopes.OpenId.Name, + StandardScopes.Profile.Name, + Constants.ApiScope + }, + RequireConsent = false + }; + } + } +} diff --git a/src/Squidex/Configurations/Identity/MyIdentityOptions.cs b/src/Squidex/Configurations/Identity/MyIdentityOptions.cs index e36503697..fce5cae55 100644 --- a/src/Squidex/Configurations/Identity/MyIdentityOptions.cs +++ b/src/Squidex/Configurations/Identity/MyIdentityOptions.cs @@ -5,10 +5,13 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Configurations.Identity { public sealed class MyIdentityOptions { + public string BaseUrl { get; set; } + public string DefaultUsername { get; set; } public string DefaultPassword { get; set; } @@ -17,8 +20,11 @@ namespace Squidex.Configurations.Identity public string GoogleSecret { get; set; } - public string BaseUrl { get; set; } - public bool RequiresHttps { get; set; } + + public string BuildUrl(string path) + { + return $"{BaseUrl.TrimEnd('/')}/{path.Trim('/')}/"; + } } } diff --git a/src/Squidex/Configurations/Domain/InfrastructureUsage.cs b/src/Squidex/Configurations/Web/WebModule.cs similarity index 55% rename from src/Squidex/Configurations/Domain/InfrastructureUsage.cs rename to src/Squidex/Configurations/Web/WebModule.cs index 1d116130c..de781f245 100644 --- a/src/Squidex/Configurations/Domain/InfrastructureUsage.cs +++ b/src/Squidex/Configurations/Web/WebModule.cs @@ -1,23 +1,23 @@ // ========================================================================== -// InfrastructureUsage.cs +// WebModule.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Microsoft.AspNetCore.Builder; +using Autofac; using Squidex.Pipeline; -namespace Squidex.Configurations.Domain +namespace Squidex.Configurations.Web { - public static class InfrastructureUsage + public class WebModule : Module { - public static IApplicationBuilder UseMyApps(this IApplicationBuilder app) + protected override void Load(ContainerBuilder builder) { - app.UseMiddleware(); - - return app; + builder.RegisterType() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Modules/Api/Apps/AppController.cs b/src/Squidex/Modules/Api/Apps/AppController.cs index 7beae53ae..644c0ba9a 100644 --- a/src/Squidex/Modules/Api/Apps/AppController.cs +++ b/src/Squidex/Modules/Api/Apps/AppController.cs @@ -23,7 +23,6 @@ namespace Squidex.Modules.Api.Apps { [Authorize] [ApiExceptionFilter] - [DeactivateForAppDomain] public class AppController : ControllerBase { private readonly IAppRepository appRepository; @@ -35,7 +34,7 @@ namespace Squidex.Modules.Api.Apps } [HttpGet] - [Route("api/apps/")] + [Route("apps/")] public async Task> Query() { var schemas = await appRepository.QueryAllAsync(); @@ -44,7 +43,7 @@ namespace Squidex.Modules.Api.Apps } [HttpPost] - [Route("api/apps/")] + [Route("apps/")] public async Task Create([FromBody] CreateAppDto model) { var command = SimpleMapper.Map(model, new CreateApp { AggregateId = Guid.NewGuid() }); diff --git a/src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs index 27c5d1785..97708ec34 100644 --- a/src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -16,7 +17,10 @@ using Squidex.Write.Schemas.Commands; namespace Squidex.Modules.Api.Schemas { + [Authorize] [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + [Route("api/apps/{app}")] public class SchemasFieldsController : ControllerBase { public SchemasFieldsController(ICommandBus commandBus) @@ -25,7 +29,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPost] - [Route("api/schemas/{name}/fields/")] + [Route("schemas/{name}/fields/")] public Task Add(string name, [FromBody] CreateFieldDto model) { var command = SimpleMapper.Map(model, new AddField()); @@ -34,7 +38,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPut] - [Route("api/schemas/{name}/fields/{fieldId:long}/")] + [Route("schemas/{name}/fields/{fieldId:long}/")] public Task Update(string name, long fieldId, [FromBody] UpdateFieldDto model) { var command = SimpleMapper.Map(model, new UpdateField()); @@ -43,7 +47,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPut] - [Route("api/schemas/{name}/fields/{fieldId:long}/hide/")] + [Route("schemas/{name}/fields/{fieldId:long}/hide/")] public Task Hide(string name, long fieldId) { var command = new HideField { FieldId = fieldId }; @@ -52,7 +56,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPut] - [Route("api/schemas/{name}/fields/{fieldId:long}/show/")] + [Route("schemas/{name}/fields/{fieldId:long}/show/")] public Task Show(string name, long fieldId) { var command = new ShowField { FieldId = fieldId }; @@ -61,7 +65,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPut] - [Route("api/schemas/{name}/fields/{fieldId:long}/enable/")] + [Route("schemas/{name}/fields/{fieldId:long}/enable/")] public Task Enable(string name, long fieldId) { var command = new EnableField { FieldId = fieldId }; @@ -70,7 +74,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpPut] - [Route("api/schemas/{name}/fields/{fieldId:long}/disable/")] + [Route("schemas/{name}/fields/{fieldId:long}/disable/")] public Task Disable(string name, long fieldId) { var command = new DisableField { FieldId = fieldId }; @@ -79,7 +83,7 @@ namespace Squidex.Modules.Api.Schemas } [HttpDelete] - [Route("api/schemas/{name}/fields/{fieldId:long}/")] + [Route("schemas/{name}/fields/{fieldId:long}/")] public Task Delete(string name, long fieldId) { var command = new DeleteField { FieldId = fieldId }; diff --git a/src/Squidex/Modules/UI/Account/AccountController.cs b/src/Squidex/Modules/UI/Account/AccountController.cs index 0810d7451..e4dce6e7a 100644 --- a/src/Squidex/Modules/UI/Account/AccountController.cs +++ b/src/Squidex/Modules/UI/Account/AccountController.cs @@ -38,14 +38,14 @@ namespace Squidex.Modules.UI.Account } [HttpGet] - [Route("account/client-silent/")] + [Route("client-callback-silent/")] public IActionResult ClientSilent() { return View(); } [HttpGet] - [Route("account/client-popup/")] + [Route("client-callback-popup/")] public IActionResult ClientPopup() { return View(); diff --git a/src/Squidex/Pipeline/AppFeature.cs b/src/Squidex/Pipeline/AppFeature.cs index da06a4f93..e16be9da3 100644 --- a/src/Squidex/Pipeline/AppFeature.cs +++ b/src/Squidex/Pipeline/AppFeature.cs @@ -1,3 +1,11 @@ +// ========================================================================== +// AppFeature.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + using System; namespace Squidex.Pipeline diff --git a/src/Squidex/Pipeline/AppFilterAttribute.cs b/src/Squidex/Pipeline/AppFilterAttribute.cs new file mode 100644 index 000000000..143ca8185 --- /dev/null +++ b/src/Squidex/Pipeline/AppFilterAttribute.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// AppMiddleware.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Read.Apps.Services; + +namespace Squidex.Pipeline +{ + public sealed class AppFilterAttribute : ActionFilterAttribute + { + private readonly IAppProvider appProvider; + + public AppFilterAttribute(IAppProvider appProvider) + { + this.appProvider = appProvider; + } + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var appName = context.RouteData.Values["app"]?.ToString(); + + if (!string.IsNullOrWhiteSpace(appName)) + { + var appId = await appProvider.FindAppIdByNameAsync(appName); + + if (!appId.HasValue) + { + context.Result = new NotFoundResult(); + return; + } + + if (!context.HttpContext.User.HasClaim("app", appName)) + { + context.Result = new NotFoundResult(); + return; + } + + context.HttpContext.Features.Set(new AppFeature(appId.Value)); + } + + await next(); + } + } +} diff --git a/src/Squidex/Pipeline/AppMiddleware.cs b/src/Squidex/Pipeline/AppMiddleware.cs deleted file mode 100644 index eaadadc92..000000000 --- a/src/Squidex/Pipeline/AppMiddleware.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// AppMiddleware.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Squidex.Read.Apps.Services; - -namespace Squidex.Pipeline -{ - public sealed class AppMiddleware - { - private readonly IAppProvider appProvider; - private readonly IHostingEnvironment appEnvironment; - private readonly RequestDelegate next; - - public AppMiddleware(RequestDelegate next, IAppProvider appProvider, IHostingEnvironment appEnvironment) - { - this.next = next; - this.appProvider = appProvider; - this.appEnvironment = appEnvironment; - } - - public async Task Invoke(HttpContext context) - { - var hostParts = context.Request.Host.ToString().Split('.'); - - if (appEnvironment.IsDevelopment() || hostParts.Length >= 3) - { - var appId = await appProvider.FindAppIdByNameAsync(hostParts[0]); - - if (appId.HasValue) - { - context.Features.Set(new AppFeature(appId.Value)); - } - } - - await next(context); - } - } -} diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 1f57f5a74..2b8ea00a4 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -15,18 +15,28 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Squidex.Configurations; using Squidex.Configurations.Domain; using Squidex.Configurations.EventStore; using Squidex.Configurations.Identity; using Squidex.Configurations.Web; using Squidex.Store.MongoDb; - +using System.Linq; +using Microsoft.AspNetCore.Http; +// ReSharper disable ConvertClosureToMethodGroup // ReSharper disable AccessToModifiedClosure namespace Squidex { public class Startup { + private static readonly string[] IdentityServerPaths = + { + "/client-callback-popup", + "/client-callback-silent", + "/account" + }; + public IConfigurationRoot Configuration { get; } public IHostingEnvironment Environment { get; } @@ -65,10 +75,11 @@ namespace Squidex Configuration.GetSection("identity")); var builder = new ContainerBuilder(); - builder.RegisterModule(); builder.RegisterModule(); + builder.RegisterModule(); builder.RegisterModule(); builder.RegisterModule(); + builder.RegisterModule(); builder.RegisterModule(); builder.Populate(services); @@ -91,16 +102,70 @@ namespace Squidex app.UseDefaultFiles(new DefaultFilesOptions { DefaultFileNames = new List { "build/index.html" } }); } - app.UseMyDefaultUser(); - app.UseMyEventStore(); - app.UseMyIdentity(); - app.UseMyIdentityServer(); - app.UseMyApiProtection(); - app.UseMyGoogleAuthentication(); - app.UseMyApps(); + UseIdentity(app); + UseApi(app); + UseFrontend(app); + } + + private void UseIdentity(IApplicationBuilder app) + { + app.Map(Constants.IdentityPrefix, identityApp => + { + if (Environment.IsDevelopment()) + { + identityApp.UseDeveloperExceptionPage(); + } + + identityApp.UseMyIdentity(); + identityApp.UseMyIdentityServer(); + identityApp.UseMyApiProtection(); + identityApp.UseMyGoogleAuthentication(); + identityApp.UseStaticFiles(); + + identityApp.MapWhen(x => IsIdentityRequest(x), mvcApp => + { + mvcApp.UseMvc(); + }); + }); + } + + private void UseApi(IApplicationBuilder app) + { + app.Map(Constants.ApiPrefix, appApi => + { + if (Environment.IsDevelopment()) + { + appApi.UseDeveloperExceptionPage(); + } + + appApi.UseMyApiProtection(); + + appApi.MapWhen(x => !IsIdentityRequest(x), mvcApp => + { + mvcApp.UseMvc(); + }); + }); + } + + private void UseFrontend(IApplicationBuilder app) + { + if (Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseWebpackProxy(); + app.UseDefaultFiles(); + } + else + { + app.UseDefaultFiles(new DefaultFilesOptions { DefaultFileNames = new List { "build/index.html" } }); + } + app.UseStaticFiles(); - app.UseMvc(); + } + private static bool IsIdentityRequest(HttpContext context) + { + return IdentityServerPaths.Any(p => context.Request.Path.StartsWithSegments(p)); } } } diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index f827bf273..46a7e0053 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -34,10 +34,12 @@ export class AuthService { Log.logger = console; this.userManager = new UserManager({ - client_id: 'management-portal', - silent_redirect_uri: apiUrl.buildUrl('account/client-silent'), - popup_redirect_uri: apiUrl.buildUrl('account/client-popup'), - authority: apiUrl.buildUrl('/'), + client_id: 'squidex-frontend', + silent_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-silent/'), + popup_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-popup/'), + authority: apiUrl.buildUrl('identity-server/'), + response_type: 'id_token token', + scope: 'openid profile squidex-api' }); this.userManager.getUser()