From 7bbcb04b6a01d246a10f1806c7ca6e99713ce836 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 Aug 2019 14:25:40 +0200 Subject: [PATCH] Feature/user clients (#405) * First test implementation. * Added sub to client. --- .../UserManagerExtensions.cs | 8 +++ .../Identity/SquidexClaimTypes.cs | 2 + src/Squidex.Shared/Users/UserExtensions.cs | 5 ++ .../IdentityServer/Config/LazyClientStore.cs | 71 +++++++++++++++---- .../Controllers/Profile/ProfileController.cs | 9 +++ .../Controllers/Profile/ProfileVM.cs | 2 + .../Views/Profile/Profile.cshtml | 33 ++++++++- 7 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index 2db94237f..94176bfc0 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -12,6 +12,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Squidex.Infrastructure; +using Squidex.Shared.Identity; namespace Squidex.Domain.Users { @@ -159,6 +160,13 @@ namespace Squidex.Domain.Users return await userManager.ResolveUserAsync(user); } + public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) + { + var claims = new[] { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) }; + + return userManager.SyncClaimsAsync(user, claims); + } + public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) { try diff --git a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/src/Squidex.Shared/Identity/SquidexClaimTypes.cs index 7112350b8..84078c4a2 100644 --- a/src/Squidex.Shared/Identity/SquidexClaimTypes.cs +++ b/src/Squidex.Shared/Identity/SquidexClaimTypes.cs @@ -25,6 +25,8 @@ namespace Squidex.Shared.Identity public static readonly string PermissionsClient = "client_urn:squidex:permissions"; + public static readonly string ClientSecret = "urn:squidex:clientSecret"; + public static readonly string Prefix = "urn:squidex:"; public static readonly string PrefixClient = "client_urn:squidex:"; diff --git a/src/Squidex.Shared/Users/UserExtensions.cs b/src/Squidex.Shared/Users/UserExtensions.cs index 52f5a5de5..f8c28c51c 100644 --- a/src/Squidex.Shared/Users/UserExtensions.cs +++ b/src/Squidex.Shared/Users/UserExtensions.cs @@ -54,6 +54,11 @@ namespace Squidex.Shared.Users return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); } + public static string ClientSecret(this IUser user) + { + return user.GetClaimValue(SquidexClaimTypes.ClientSecret); + } + public static string PictureUrl(this IUser user) { return user.GetClaimValue(SquidexClaimTypes.PictureUrl); diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index e07e6d059..4911496fc 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -12,30 +12,39 @@ using System.Threading.Tasks; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Stores; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Users; using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Shared.Identity; +using Squidex.Shared.Users; using Squidex.Web; namespace Squidex.Areas.IdentityServer.Config { public class LazyClientStore : IClientStore { + private readonly UserManager userManager; private readonly IAppProvider appProvider; private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); public LazyClientStore( + UserManager userManager, IOptions urlsOptions, IOptions identityOptions, IAppProvider appProvider) { + Guard.NotNull(identityOptions, nameof(identityOptions)); Guard.NotNull(urlsOptions, nameof(urlsOptions)); + Guard.NotNull(userManager, nameof(userManager)); Guard.NotNull(appProvider, nameof(appProvider)); + this.userManager = userManager; this.appProvider = appProvider; CreateStaticClients(urlsOptions, identityOptions); @@ -52,23 +61,52 @@ namespace Squidex.Areas.IdentityServer.Config var (appName, appClientId) = clientId.GetClientParts(); - if (appName == null) + if (!string.IsNullOrWhiteSpace(appName)) { - return null; - } + var app = await appProvider.GetAppAsync(appName); - var app = await appProvider.GetAppAsync(appName); + var appClient = app?.Clients.GetOrDefault(appClientId); - var appClient = app?.Clients.GetOrDefault(appClientId); + if (appClient != null) + { + return CreateClientFromApp(clientId, appClient); + } + } - if (appClient == null) + var user = await userManager.FindByIdWithClaimsAsync(clientId); + + if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) { - return null; + return CreateClientFromUser(user); } - client = CreateClientFromApp(clientId, appClient); + return null; + } - return client; + private Client CreateClientFromUser(UserWithClaims user) + { + return new Client + { + ClientId = user.Id, + ClientName = $"{user.Email} Client", + ClientClaimsPrefix = null, + ClientSecrets = new List + { + new Secret(user.ClientSecret().Sha256()) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AllowedScopes = new List + { + Constants.ApiScope, + Constants.RoleScope, + Constants.PermissionsScope + }, + Claims = new List + { + new Claim(OpenIdClaims.Subject, user.Id) + } + }; } private static Client CreateClientFromApp(string id, AppClient appClient) @@ -77,7 +115,10 @@ namespace Squidex.Areas.IdentityServer.Config { ClientId = id, ClientName = id, - ClientSecrets = new List { new Secret(appClient.Secret.Sha256()) }, + ClientSecrets = new List + { + new Secret(appClient.Secret.Sha256()) + }, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = new List @@ -136,7 +177,10 @@ namespace Squidex.Areas.IdentityServer.Config { ClientId = internalClient, ClientName = internalClient, - ClientSecrets = new List { new Secret(Constants.InternalClientSecret) }, + ClientSecrets = new List + { + new Secret(Constants.InternalClientSecret) + }, RedirectUris = new List { urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false), @@ -165,7 +209,10 @@ namespace Squidex.Areas.IdentityServer.Config { ClientId = id, ClientName = id, - ClientSecrets = new List { new Secret(identityOptions.AdminClientSecret.Sha256()) }, + ClientSecrets = new List + { + new Secret(identityOptions.AdminClientSecret.Sha256()) + }, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = new List diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 21808f9fe..e28258681 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -110,6 +110,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile "Password changed successfully."); } + [HttpPost] + [Route("/account/profile/generate-client-secret/")] + public Task GenerateClientSecret() + { + return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity), + "Client secret generated successfully."); + } + [HttpPost] [Route("/account/profile/upload-picture/")] public Task UploadPicture(List file) @@ -193,6 +201,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile var result = new ProfileVM { Id = user.Id, + ClientSecret = user.ClientSecret(), Email = user.Email, ErrorMessage = errorMessage, ExternalLogins = taskForLogins.Result, diff --git a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs index e65519f73..e895f70e9 100644 --- a/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -18,6 +18,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public string DisplayName { get; set; } + public string ClientSecret { get; set; } + public string ErrorMessage { get; set; } public string SuccessMessage { get; set; } diff --git a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml index 945e7c4c7..71a2d9411 100644 --- a/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml @@ -216,6 +216,34 @@ } +
+

Client

+ + Use the client credentials to call the API with your profile information and permissions. + +
+
+ + + +
+
+
+
+ + + +
+
+ + +
+ +
+
+
+
+ \ No newline at end of file