Browse Source

Feature/user clients (#405)

* First test implementation.

* Added sub to client.
pull/408/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
7bbcb04b6a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  2. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  3. 5
      src/Squidex.Shared/Users/UserExtensions.cs
  4. 71
      src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  5. 9
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  6. 2
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs
  7. 33
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

8
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<IdentityResult> GenerateClientSecretAsync(this UserManager<IdentityUser> userManager, IdentityUser user)
{
var claims = new[] { new Claim(SquidexClaimTypes.ClientSecret, RandomHash.New()) };
return userManager.SyncClaimsAsync(user, claims);
}
public static async Task<IdentityResult> UpdateSafeAsync(this UserManager<IdentityUser> userManager, IdentityUser user, UserValues values)
{
try

2
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:";

5
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);

71
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<IdentityUser> userManager;
private readonly IAppProvider appProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(
UserManager<IdentityUser> userManager,
IOptions<UrlsOptions> urlsOptions,
IOptions<MyIdentityOptions> 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<Secret>
{
new Secret(user.ClientSecret().Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<Claim>
{
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<Secret> { new Secret(appClient.Secret.Sha256()) },
ClientSecrets = new List<Secret>
{
new Secret(appClient.Secret.Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
@ -136,7 +177,10 @@ namespace Squidex.Areas.IdentityServer.Config
{
ClientId = internalClient,
ClientName = internalClient,
ClientSecrets = new List<Secret> { new Secret(Constants.InternalClientSecret) },
ClientSecrets = new List<Secret>
{
new Secret(Constants.InternalClientSecret)
},
RedirectUris = new List<string>
{
urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false),
@ -165,7 +209,10 @@ namespace Squidex.Areas.IdentityServer.Config
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret> { new Secret(identityOptions.AdminClientSecret.Sha256()) },
ClientSecrets = new List<Secret>
{
new Secret(identityOptions.AdminClientSecret.Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>

9
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<IActionResult> GenerateClientSecret()
{
return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user.Identity),
"Client secret generated successfully.");
}
[HttpPost]
[Route("/account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> 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,

2
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; }

33
src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -216,6 +216,34 @@
</div>
}
<div class="profile-section">
<h2>Client</h2>
<small class="form-text text-muted mt-2 mb-2">Use the client credentials to call the API with your profile information and permissions.</small>
<div class="row no-gutters form-group">
<div class="col-8">
<label for="clientId">Client Id</label>
<input class="form-control" name="clientId" id="clientId" readonly value="@Model.Id" />
</div>
</div>
<div class="row no-gutters form-group">
<div class="col-8">
<label for="clientSecret">Client Secret</label>
<input class="form-control" name="clientSecret" id="clientSecret" readonly value="@Model.ClientSecret" />
</div>
<div class="col-4 pl-2">
<label for="generate">&nbsp;</label>
<form class="profile-form" asp-controller="Profile" asp-action="GenerateClientSecret" method="post">
<button type="submit" class="btn btn-success btn-block">Generate</button>
</form>
</div>
</div>
</div>
<script>
var pictureButton = document.getElementById('pictureButton');
var pictureInput = document.getElementById('pictureInput');
@ -235,8 +263,7 @@
if (successMessage) {
setTimeout(function() {
successMessage.remove();
},
5000);
successMessage.remove();
}, 5000);
}
</script>
Loading…
Cancel
Save