From dc5a6842652369a279ee5897e38eea7c03261c7d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 17 Jul 2019 18:22:49 +0200 Subject: [PATCH] More OIDC options. --- .../Commands/ReadonlyCommandMiddleware.cs | 6 +-- .../Pipeline/EnforceHttpsMiddleware.cs | 6 +-- .../Controllers/Assets/AssetsController.cs | 8 ++-- .../Contents/ContentsController.cs | 10 ++--- .../Controllers/Account/AccountController.cs | 14 +++--- .../Controllers/Profile/ProfileController.cs | 6 +-- .../Config/Authentication/OidcHandler.cs | 44 +++++++++++++++++++ .../Config/Authentication/OidcServices.cs | 12 ++++- src/Squidex/Config/MyIdentityOptions.cs | 12 ++++- src/Squidex/appsettings.json | 3 ++ 10 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 src/Squidex/Config/Authentication/OidcHandler.cs diff --git a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs index dcd1ca88b..c4c5ed3e9 100644 --- a/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs +++ b/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs @@ -13,18 +13,18 @@ namespace Squidex.Infrastructure.Commands { public sealed class ReadonlyCommandMiddleware : ICommandMiddleware { - private readonly IOptions options; + private readonly ReadonlyOptions options; public ReadonlyCommandMiddleware(IOptions options) { Guard.NotNull(options, nameof(options)); - this.options = options; + this.options = options.Value; } public Task HandleAsync(CommandContext context, Func next) { - if (options.Value.IsReadonly) + if (options.IsReadonly) { throw new DomainException("Application is in readonly mode at the moment."); } diff --git a/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs index 499b683f2..fc128337d 100644 --- a/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs +++ b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs @@ -14,16 +14,16 @@ namespace Squidex.Web.Pipeline { public sealed class EnforceHttpsMiddleware : IMiddleware { - private readonly IOptions urls; + private readonly UrlsOptions urls; public EnforceHttpsMiddleware(IOptions urls) { - this.urls = urls; + this.urls = urls.Value; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - if (!urls.Value.EnforceHTTPS) + if (!urls.EnforceHTTPS) { await next(context); } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 0456767f0..3131cb6f3 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Assets private readonly IAssetQueryService assetQuery; private readonly IAssetUsageTracker assetStatsRepository; private readonly IAppPlansProvider appPlansProvider; - private readonly IOptions controllerOptions; + private readonly MyContentsControllerOptions controllerOptions; private readonly ITagService tagService; private readonly AssetOptions assetOptions; @@ -55,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Assets this.assetQuery = assetQuery; this.assetStatsRepository = assetStatsRepository; this.appPlansProvider = appPlansProvider; - this.controllerOptions = controllerOptions; + this.controllerOptions = controllerOptions.Value; this.tagService = tagService; } @@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Assets return AssetsDto.FromAssets(assets, this, app); }); - if (controllerOptions.Value.EnableSurrogateKeys && assets.Count <= controllerOptions.Value.MaxItemsForSurrogateKeys) + if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys) { Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys(); } @@ -148,7 +148,7 @@ namespace Squidex.Areas.Api.Controllers.Assets return AssetDto.FromAsset(asset, this, app); }); - if (controllerOptions.Value.EnableSurrogateKeys) + if (controllerOptions.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey(); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 42bf4b1ce..9d18ca9b9 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -25,7 +25,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { public sealed class ContentsController : ApiController { - private readonly IOptions controllerOptions; + private readonly MyContentsControllerOptions controllerOptions; private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; private readonly IGraphQLService graphQl; @@ -39,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; - this.controllerOptions = controllerOptions; + this.controllerOptions = controllerOptions.Value; this.graphQl = graphQl; } @@ -205,7 +205,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var response = ContentDto.FromContent(Context, content, this); - if (controllerOptions.Value.EnableSurrogateKeys) + if (controllerOptions.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } @@ -240,7 +240,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var response = ContentDto.FromContent(Context, content, this); - if (controllerOptions.Value.EnableSurrogateKeys) + if (controllerOptions.EnableSurrogateKeys) { Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } @@ -446,7 +446,7 @@ namespace Squidex.Areas.Api.Controllers.Contents private bool ShouldProvideSurrogateKeys(IReadOnlyList response) { - return controllerOptions.Value.EnableSurrogateKeys && response.Count <= controllerOptions.Value.MaxItemsForSurrogateKeys; + return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys; } } } diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 7b2553554..fb645e1a3 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -36,7 +36,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account private readonly UserManager userManager; private readonly IUserFactory userFactory; private readonly IUserEvents userEvents; - private readonly IOptions identityOptions; + private readonly MyIdentityOptions identityOptions; private readonly ISemanticLog log; private readonly IIdentityServerInteractionService interactions; @@ -54,7 +54,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account this.userManager = userManager; this.userFactory = userFactory; this.interactions = interactions; - this.identityOptions = identityOptions; + this.identityOptions = identityOptions.Value; this.signInManager = signInManager; } @@ -97,7 +97,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account [Route("account/consent/")] public IActionResult Consent(string returnUrl = null) { - return View(new ConsentVM { PrivacyUrl = identityOptions.Value.PrivacyUrl, ReturnUrl = returnUrl }); + return View(new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }); } [HttpPost] @@ -116,7 +116,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account if (!ModelState.IsValid) { - var vm = new ConsentVM { PrivacyUrl = identityOptions.Value.PrivacyUrl, ReturnUrl = returnUrl }; + var vm = new ConsentVM { PrivacyUrl = identityOptions.PrivacyUrl, ReturnUrl = returnUrl }; return View(vm); } @@ -194,7 +194,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account private async Task LoginViewAsync(string returnUrl, bool isLogin, bool isFailed) { - var allowPasswordAuth = identityOptions.Value.AllowPasswordAuth; + var allowPasswordAuth = identityOptions.AllowPasswordAuth; var externalProviders = await signInManager.GetExternalProvidersAsync(); @@ -298,7 +298,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account { return RedirectToAction(nameof(Login)); } - else if (user != null && !user.HasConsent()) + else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) { return RedirectToAction(nameof(Consent), new { returnUrl }); } @@ -327,7 +327,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account private Task LockAsync(UserWithClaims user, bool isFirst) { - if (isFirst || !identityOptions.Value.LockAutomatically) + if (isFirst || !identityOptions.LockAutomatically) { return TaskHelper.True; } diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index c727806c6..dbb87c022 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -32,7 +32,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile private readonly UserManager userManager; private readonly IUserPictureStore userPictureStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - private readonly IOptions identityOptions; + private readonly MyIdentityOptions identityOptions; public ProfileController( SignInManager signInManager, @@ -42,7 +42,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile IOptions identityOptions) { this.signInManager = signInManager; - this.identityOptions = identityOptions; + this.identityOptions = identityOptions.Value; this.userManager = userManager; this.userPictureStore = userPictureStore; this.assetThumbnailGenerator = assetThumbnailGenerator; @@ -198,7 +198,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile DisplayName = user.DisplayName(), IsHidden = user.IsHidden(), HasPassword = taskForPassword.Result, - HasPasswordAuth = identityOptions.Value.AllowPasswordAuth, + HasPasswordAuth = identityOptions.AllowPasswordAuth, SuccessMessage = successMessage }; diff --git a/src/Squidex/Config/Authentication/OidcHandler.cs b/src/Squidex/Config/Authentication/OidcHandler.cs new file mode 100644 index 000000000..c88a56e24 --- /dev/null +++ b/src/Squidex/Config/Authentication/OidcHandler.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Squidex.Shared.Identity; + +namespace Squidex.Config.Authentication +{ + public sealed class OidcHandler : OpenIdConnectEvents + { + private readonly MyIdentityOptions options; + + public OidcHandler(MyIdentityOptions options ) + { + this.options = options; + } + + public override Task TokenValidated(TokenValidatedContext context) + { + var identity = (ClaimsIdentity)context.Principal.Identity; + + if (!string.IsNullOrWhiteSpace(options.OidcRoleClaimType) && options.OidcRoleMapping?.Count >= 0) + { + var role = identity.FindFirst(x => x.Type == options.OidcRoleClaimType)?.Value; + + if (!string.IsNullOrWhiteSpace(role) && options.OidcRoleMapping.TryGetValue(role, out var permissions) && permissions != null) + { + foreach (var permission in permissions) + { + identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); + } + } + } + + return base.TokenValidated(context); + } + } +} diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs index de3eebe6a..a096f8632 100644 --- a/src/Squidex/Config/Authentication/OidcServices.cs +++ b/src/Squidex/Config/Authentication/OidcServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Tasks; using Squidex.Web; namespace Squidex.Config.Authentication @@ -25,9 +26,16 @@ namespace Squidex.Config.Authentication options.Authority = identityOptions.OidcAuthority; options.ClientId = identityOptions.OidcClient; options.ClientSecret = identityOptions.OidcSecret; - options.Scope.Add(Constants.EmailScope); - options.Scope.Add(Constants.PermissionsScope); options.RequireHttpsMetadata = false; + options.Events = new OidcHandler(identityOptions); + + if (identityOptions.OidcScopes != null) + { + foreach (var scope in identityOptions.OidcScopes) + { + options.Scope.Add(scope); + } + } }); } diff --git a/src/Squidex/Config/MyIdentityOptions.cs b/src/Squidex/Config/MyIdentityOptions.cs index e8547ff90..3234337d8 100644 --- a/src/Squidex/Config/MyIdentityOptions.cs +++ b/src/Squidex/Config/MyIdentityOptions.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; + namespace Squidex.Config { public sealed class MyIdentityOptions @@ -37,6 +39,12 @@ namespace Squidex.Config public string OidcAuthority { get; set; } + public string OidcRoleClaimType { get; set; } + + public string[] OidcScopes { get; set; } + + public Dictionary OidcRoleMapping { get; set; } + public string AuthorityUrl { get; set; } public string PrivacyUrl { get; set; } @@ -47,6 +55,8 @@ namespace Squidex.Config public bool LockAutomatically { get; set; } + public bool NoConsent { get; set; } + public bool IsAdminConfigured() { return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword); @@ -59,7 +69,7 @@ namespace Squidex.Config public bool IsOidcConfigured() { - return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient) && !string.IsNullOrWhiteSpace(OidcSecret); + return !string.IsNullOrWhiteSpace(OidcAuthority) && !string.IsNullOrWhiteSpace(OidcClient); } public bool IsGithubAuthConfigured() diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index d60842db4..b483bf879 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -440,6 +440,9 @@ "oidcAuthority": "", "oidcClient": "", "oidcSecret": "", + "oidcScopes": [ + "email" + ], /* * Lock new users automatically, the administrator must unlock them. */