Browse Source

Migration/openiddictcore (#703)

* Started with migration.

* Progress

* Refactorings.

* Dont query scopes twice.

* Fix tests

* Test NPM bullshit.

* Remove ls

* Try older node version.

* Update version.

* Fixes for admin client.

* Test

* Logging

* Fake https.

* Run on http as well.

* Clean up again.
pull/708/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
2968bb87e1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Dockerfile
  2. 113
      backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs
  3. 1
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  4. 53
      backend/src/Squidex.Domain.Users/DefaultKeyStore.cs
  5. 12
      backend/src/Squidex.Domain.Users/InMemory/Extensions.cs
  6. 58
      backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs
  7. 45
      backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs
  8. 241
      backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs
  9. 201
      backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs
  10. 3
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  11. 12
      backend/src/Squidex.Infrastructure/Security/Extensions.cs
  12. 22
      backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  13. 28
      backend/src/Squidex.Web/Constants.cs
  14. 31
      backend/src/Squidex/Areas/Api/Config/IdentityServerPathMiddleware.cs
  15. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs
  16. 4
      backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs
  17. 5
      backend/src/Squidex/Areas/Api/Startup.cs
  18. 59
      backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs
  19. 33
      backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs
  20. 243
      backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs
  21. 38
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs
  22. 119
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  23. 253
      backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  24. 43
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  25. 287
      backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs
  26. 37
      backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs
  27. 6
      backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs
  28. 13
      backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs
  29. 68
      backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs
  30. 13
      backend/src/Squidex/Areas/IdentityServer/Startup.cs
  31. 4
      backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml
  32. 2
      backend/src/Squidex/Areas/OrleansDashboard/Startup.cs
  33. 2
      backend/src/Squidex/Areas/Portal/Startup.cs
  34. 61
      backend/src/Squidex/Config/Authentication/IdentityServerServices.cs
  35. 1
      backend/src/Squidex/Config/Authentication/OidcHandler.cs
  36. 5
      backend/src/Squidex/Config/Domain/LoggingServices.cs
  37. 19
      backend/src/Squidex/Config/Domain/StoreServices.cs
  38. 5
      backend/src/Squidex/Squidex.csproj
  39. 64
      backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs
  40. 2
      backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs
  41. 4
      frontend/app/shared/services/auth.service.ts
  42. 44
      frontend/package-lock.json
  43. 2
      frontend/package.json

4
Dockerfile

@ -36,7 +36,7 @@ RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --co
#
# Stage 2, Build Frontend
#
FROM buildkite/puppeteer:8.0.0 as frontend
FROM buildkite/puppeteer:5.2.1 as frontend
WORKDIR /src
@ -48,8 +48,6 @@ COPY frontend/package*.json /tmp/
# Copy patches for broken npm packages
COPY frontend/patches /tmp/patches
RUN cd /tmp/patches && dir
# Install Node packages
RUN cd /tmp && npm set unsafe-perm true && npm install --loglevel=error

113
backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs

@ -1,113 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Users.MongoDb.Infrastructure
{
public class MongoPersistedGrantStore : MongoRepositoryBase<PersistedGrant>, IPersistedGrantStore
{
static MongoPersistedGrantStore()
{
BsonClassMap.RegisterClassMap<PersistedGrant>(cm =>
{
cm.AutoMap();
cm.MapIdProperty(x => x.Key);
});
}
public MongoPersistedGrantStore(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "Identity_PersistedGrants";
}
protected override Task SetupCollectionAsync(IMongoCollection<PersistedGrant> collection, CancellationToken ct = default)
{
return collection.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<PersistedGrant>(Index.Ascending(x => x.ClientId)),
new CreateIndexModel<PersistedGrant>(Index.Ascending(x => x.SubjectId))
}, ct);
}
public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
{
return await Collection.Find(x => x.SubjectId == subjectId).ToListAsync();
}
public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
{
return await Collection.Find(CreateFilter(filter)).ToListAsync();
}
public Task<PersistedGrant> GetAsync(string key)
{
return Collection.Find(x => x.Key == key).FirstOrDefaultAsync();
}
public Task RemoveAllAsync(PersistedGrantFilter filter)
{
return Collection.DeleteManyAsync(CreateFilter(filter));
}
public Task RemoveAsync(string key)
{
return Collection.DeleteManyAsync(x => x.Key == key);
}
public Task StoreAsync(PersistedGrant grant)
{
return Collection.ReplaceOneAsync(x => x.Key == grant.Key, grant, UpsertReplace);
}
private static FilterDefinition<PersistedGrant> CreateFilter(PersistedGrantFilter filter)
{
var filters = new List<FilterDefinition<PersistedGrant>>();
if (!string.IsNullOrWhiteSpace(filter.ClientId))
{
filters.Add(Filter.Eq(x => x.ClientId, filter.ClientId));
}
if (!string.IsNullOrWhiteSpace(filter.SessionId))
{
filters.Add(Filter.Eq(x => x.SessionId, filter.SessionId));
}
if (!string.IsNullOrWhiteSpace(filter.SubjectId))
{
filters.Add(Filter.Eq(x => x.SubjectId, filter.SubjectId));
}
if (!string.IsNullOrWhiteSpace(filter.Type))
{
filters.Add(Filter.Eq(x => x.Type, filter.Type));
}
if (filters.Count > 0)
{
return Filter.And(filters);
}
return new BsonDocument();
}
}
}

1
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -18,7 +18,6 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.12.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

53
backend/src/Squidex.Domain.Users/DefaultKeyStore.cs

@ -6,23 +6,20 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users
{
public sealed class DefaultKeyStore : ISigningCredentialStore, IValidationKeysStore
public sealed class DefaultKeyStore : IConfigureOptions<OpenIddictServerOptions>
{
private readonly ISnapshotStore<State> store;
private SigningCredentials? cachedKey;
private SecurityKeyInfo[]? cachedKeyInfo;
[CollectionName("Identity_Keys")]
public sealed class State
@ -39,32 +36,28 @@ namespace Squidex.Domain.Users
this.store = store;
}
public async Task<SigningCredentials> GetSigningCredentialsAsync()
public void Configure(OpenIddictServerOptions options)
{
var (_, key) = await GetOrCreateKeyAsync();
var securityKey = GetOrCreateKeyAsync().Result;
return key;
}
public async Task<IEnumerable<SecurityKeyInfo>> GetValidationKeysAsync()
{
var (info, _) = await GetOrCreateKeyAsync();
options.SigningCredentials.Add(
new SigningCredentials(securityKey,
SecurityAlgorithms.RsaSha256));
return info;
options.EncryptionCredentials.Add(new EncryptingCredentials(securityKey,
SecurityAlgorithms.RsaOAEP,
SecurityAlgorithms.Aes256CbcHmacSha512));
}
private async Task<(SecurityKeyInfo[], SigningCredentials)> GetOrCreateKeyAsync()
private async Task<RsaSecurityKey> GetOrCreateKeyAsync()
{
if (cachedKey != null && cachedKeyInfo != null)
{
return (cachedKeyInfo, cachedKey);
}
var (state, _, _) = await store.ReadAsync(default);
RsaSecurityKey securityKey;
if (state == null)
var attempts = 0;
while (state == null && attempts < 10)
{
securityKey = new RsaSecurityKey(RSA.Create(2048))
{
@ -88,7 +81,7 @@ namespace Squidex.Domain.Users
{
await store.WriteAsync(default, state, 0, 0);
return CreateCredentialsPair(securityKey);
return securityKey;
}
catch (InconsistentStateException)
{
@ -106,19 +99,7 @@ namespace Squidex.Domain.Users
KeyId = state.Key
};
return CreateCredentialsPair(securityKey);
}
private (SecurityKeyInfo[], SigningCredentials) CreateCredentialsPair(RsaSecurityKey securityKey)
{
cachedKey = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
cachedKeyInfo = new[]
{
new SecurityKeyInfo { Key = cachedKey.Key, SigningAlgorithm = cachedKey.Algorithm }
};
return (cachedKeyInfo, cachedKey);
return securityKey;
}
}
}

12
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs → backend/src/Squidex.Domain.Users/InMemory/Extensions.cs

@ -5,17 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
namespace Squidex.Areas.IdentityServer.Config
namespace Squidex.Domain.Users.InMemory
{
public static class IdentityServerExtensions
public static class Extensions
{
public static IApplicationBuilder UseSquidexIdentityServer(this IApplicationBuilder app)
public static ValueTask<T> AsValueTask<T>(this T value)
{
app.UseIdentityServer();
return app;
return new ValueTask<T>(value);
}
}
}

58
backend/src/Squidex.Domain.Users/InMemory/ImmutableApplication.cs

@ -0,0 +1,58 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using OpenIddict.Abstractions;
namespace Squidex.Domain.Users.InMemory
{
public sealed class ImmutableApplication
{
public string Id { get; }
public string? ClientId { get; }
public string? ClientSecret { get; }
public string? ConsentType { get; }
public string? DisplayName { get; }
public string? Type { get; }
public ImmutableDictionary<CultureInfo, string> DisplayNames { get; }
public ImmutableArray<string> Permissions { get; }
public ImmutableArray<string> PostLogoutRedirectUris { get; }
public ImmutableArray<string> RedirectUris { get; }
public ImmutableArray<string> Requirements { get; }
public ImmutableDictionary<string, JsonElement> Properties { get; }
public ImmutableApplication(string id, OpenIddictApplicationDescriptor descriptor)
{
Id = id;
ClientId = descriptor.ClientId;
ClientSecret = descriptor.ClientSecret;
ConsentType = descriptor.ConsentType;
DisplayName = descriptor.DisplayName;
DisplayNames = descriptor.DisplayNames.ToImmutableDictionary();
Permissions = descriptor.Permissions.ToImmutableArray();
PostLogoutRedirectUris = descriptor.PostLogoutRedirectUris.Select(x => x.ToString()).ToImmutableArray();
Properties = descriptor.Properties.ToImmutableDictionary();
RedirectUris = descriptor.RedirectUris.Select(x => x.ToString()).ToImmutableArray();
Requirements = descriptor.Requirements.ToImmutableArray();
Type = descriptor.Type;
}
}
}

45
backend/src/Squidex.Domain.Users/InMemory/ImmutableScope.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json;
using OpenIddict.Abstractions;
namespace Squidex.Domain.Users.InMemory
{
public sealed class ImmutableScope
{
public string Id { get; }
public string? Name { get; }
public string? Description { get; }
public string? DisplayName { get; }
public ImmutableDictionary<CultureInfo, string> Descriptions { get; }
public ImmutableDictionary<CultureInfo, string> DisplayNames { get; }
public ImmutableDictionary<string, JsonElement> Properties { get; }
public ImmutableArray<string> Resources { get; }
public ImmutableScope(string id, OpenIddictScopeDescriptor descriptor)
{
Id = id;
Description = descriptor.Description;
Descriptions = descriptor.Descriptions.ToImmutableDictionary();
Name = descriptor.Name;
DisplayName = descriptor.DisplayName;
DisplayNames = descriptor.DisplayNames.ToImmutableDictionary();
Properties = descriptor.Properties.ToImmutableDictionary();
Resources = descriptor.Resources.ToImmutableArray();
}
}
}

241
backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs

@ -0,0 +1,241 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using OpenIddict.Abstractions;
namespace Squidex.Domain.Users.InMemory
{
public class InMemoryApplicationStore : IOpenIddictApplicationStore<ImmutableApplication>
{
private readonly List<ImmutableApplication> applications;
public InMemoryApplicationStore(params (string Id, OpenIddictApplicationDescriptor Descriptor)[] applications)
{
this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList();
}
public InMemoryApplicationStore(IEnumerable<(string Id, OpenIddictApplicationDescriptor Descriptor)> applications)
{
this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList();
}
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken)
{
return new ValueTask<long>(applications.Count);
}
public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
return query(applications.AsQueryable()).LongCount().AsValueTask();
}
public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
{
var result = query(applications.AsQueryable(), state).First();
return result.AsValueTask();
}
public virtual ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
var result = applications.Find(x => x.Id == identifier);
return result.AsValueTask();
}
public virtual ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, CancellationToken cancellationToken)
{
var result = applications.Find(x => x.ClientId == identifier);
return result.AsValueTask();
}
public virtual async IAsyncEnumerable<ImmutableApplication> FindByPostLogoutRedirectUriAsync(string address, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = applications.Where(x => x.PostLogoutRedirectUris.Contains(address));
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<ImmutableApplication> FindByRedirectUriAsync(string address, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = applications.Where(x => x.RedirectUris.Contains(address));
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<ImmutableApplication> ListAsync(int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = applications;
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableApplication>, TState, IQueryable<TResult>> query, TState state, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = query(applications.AsQueryable(), state);
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual ValueTask<string?> GetIdAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return new ValueTask<string?>(application.Id);
}
public virtual ValueTask<string?> GetClientIdAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.ClientId.AsValueTask();
}
public virtual ValueTask<string?> GetClientSecretAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.ClientSecret.AsValueTask();
}
public virtual ValueTask<string?> GetClientTypeAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.Type.AsValueTask();
}
public virtual ValueTask<string?> GetConsentTypeAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.ConsentType.AsValueTask();
}
public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.DisplayName.AsValueTask();
}
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.DisplayNames.AsValueTask();
}
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.Permissions.AsValueTask();
}
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.PostLogoutRedirectUris.AsValueTask();
}
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.RedirectUris.AsValueTask();
}
public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.Requirements.AsValueTask();
}
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
return application.Properties.AsValueTask();
}
public virtual ValueTask CreateAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask UpdateAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask DeleteAsync(ImmutableApplication application, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask<ImmutableApplication> InstantiateAsync(CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetClientIdAsync(ImmutableApplication application, string? identifier, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetClientSecretAsync(ImmutableApplication application, string? secret, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetClientTypeAsync(ImmutableApplication application, string? type, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetConsentTypeAsync(ImmutableApplication application, string? type, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDisplayNameAsync(ImmutableApplication application, string? name, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDisplayNamesAsync(ImmutableApplication application, ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetPermissionsAsync(ImmutableApplication application, ImmutableArray<string> permissions, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetPostLogoutRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetRedirectUrisAsync(ImmutableApplication application, ImmutableArray<string> addresses, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetPropertiesAsync(ImmutableApplication application, ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetRequirementsAsync(ImmutableApplication application, ImmutableArray<string> requirements, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
}
}

201
backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs

@ -0,0 +1,201 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using OpenIddict.Abstractions;
namespace Squidex.Domain.Users.InMemory
{
public class InMemoryScopeStore : IOpenIddictScopeStore<ImmutableScope>
{
private readonly List<ImmutableScope> scopes;
public InMemoryScopeStore(params (string Id, OpenIddictScopeDescriptor Descriptor)[] scopes)
{
this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList();
}
public InMemoryScopeStore(IEnumerable<(string Id, OpenIddictScopeDescriptor Descriptor)> scopes)
{
this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList();
}
public virtual ValueTask<long> CountAsync(CancellationToken cancellationToken)
{
return new ValueTask<long>(scopes.Count);
}
public virtual ValueTask<long> CountAsync<TResult>(Func<IQueryable<ImmutableScope>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{
return query(scopes.AsQueryable()).LongCount().AsValueTask();
}
public virtual ValueTask<TResult> GetAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
{
var result = query(scopes.AsQueryable(), state).First();
return result.AsValueTask();
}
public virtual ValueTask<ImmutableScope?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
var result = scopes.Find(x => x.Id == identifier);
return result.AsValueTask();
}
public virtual ValueTask<ImmutableScope?> FindByNameAsync(string name, CancellationToken cancellationToken)
{
var result = scopes.Find(x => x.Name == name);
return result.AsValueTask();
}
public virtual async IAsyncEnumerable<ImmutableScope> FindByNamesAsync(ImmutableArray<string> names, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = scopes.Where(x => x.Name != null && names.Contains(x.Name));
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<ImmutableScope> FindByResourceAsync(string resource, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = scopes.Where(x => x.Resources.Contains(resource));
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<ImmutableScope> ListAsync(int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = scopes;
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(Func<IQueryable<ImmutableScope>, TState, IQueryable<TResult>> query, TState state, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var result = query(scopes.AsQueryable(), state);
foreach (var item in result)
{
yield return await Task.FromResult(item);
}
}
public virtual ValueTask<string?> GetIdAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return new ValueTask<string?>(scope.Id);
}
public virtual ValueTask<string?> GetNameAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.Name.AsValueTask();
}
public virtual ValueTask<string?> GetDescriptionAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.Description.AsValueTask();
}
public virtual ValueTask<string?> GetDisplayNameAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.DisplayName.AsValueTask();
}
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.Descriptions.AsValueTask();
}
public virtual ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.DisplayNames.AsValueTask();
}
public virtual ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.Properties.AsValueTask();
}
public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
return scope.Resources.AsValueTask();
}
public virtual ValueTask CreateAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask UpdateAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask DeleteAsync(ImmutableScope scope, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask<ImmutableScope> InstantiateAsync(CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDescriptionAsync(ImmutableScope scope, string? description, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDescriptionsAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> descriptions, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDisplayNameAsync(ImmutableScope scope, string? name, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetDisplayNamesAsync(ImmutableScope scope, ImmutableDictionary<CultureInfo, string> names, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetNameAsync(ImmutableScope scope, string? name, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetPropertiesAsync(ImmutableScope scope, ImmutableDictionary<string, JsonElement> properties, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public virtual ValueTask SetResourcesAsync(ImmutableScope scope, ImmutableArray<string> resources, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
}
}

3
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -16,9 +16,10 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityModel" Version="5.1.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.5" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="3.0.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="1.0.8" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

12
backend/src/Squidex.Infrastructure/Security/Extensions.cs

@ -17,18 +17,18 @@ namespace Squidex.Infrastructure.Security
{
var subjectId = principal.OpenIdSubject();
if (!string.IsNullOrWhiteSpace(subjectId))
{
return RefToken.User(subjectId);
}
var clientId = principal.OpenIdClientId();
if (!string.IsNullOrWhiteSpace(clientId))
if (!string.IsNullOrWhiteSpace(clientId) && (string.Equals(clientId, subjectId, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(subjectId)))
{
return RefToken.Client(clientId);
}
if (!string.IsNullOrWhiteSpace(subjectId))
{
return RefToken.User(subjectId);
}
return null;
}

22
backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -9,26 +9,26 @@ namespace Squidex.Shared.Identity
{
public static class SquidexClaimTypes
{
public static readonly string ClientSecret = "urn:squidex:clientSecret";
public const string ClientSecret = "urn:squidex:clientSecret";
public static readonly string Consent = "urn:squidex:consent";
public const string Consent = "urn:squidex:consent";
public static readonly string ConsentForEmails = "urn:squidex:consent:emails";
public const string ConsentForEmails = "urn:squidex:consent:emails";
public static readonly string CustomPrefix = "urn:squidex:custom";
public const string CustomPrefix = "urn:squidex:custom";
public static readonly string DisplayName = "urn:squidex:name";
public const string DisplayName = "urn:squidex:name";
public static readonly string Hidden = "urn:squidex:hidden";
public const string Hidden = "urn:squidex:hidden";
public static readonly string Invited = "urn:squidex:invited";
public const string Invited = "urn:squidex:invited";
public static readonly string NotifoKey = "urn:squidex:notifo";
public const string NotifoKey = "urn:squidex:notifo";
public static readonly string Permissions = "urn:squidex:permissions";
public const string Permissions = "urn:squidex:permissions";
public static readonly string PictureUrl = "urn:squidex:picture";
public const string PictureUrl = "urn:squidex:picture";
public static readonly string PictureUrlStore = "store";
public const string PictureUrlStore = "store";
}
}

28
backend/src/Squidex.Web/Constants.cs

@ -14,32 +14,30 @@ namespace Squidex.Web
{
public static readonly string SecurityDefinition = "squidex-oauth-auth";
public static readonly string ApiPrefix = "/api";
public static readonly string ApiScope = "squidex-api";
public static readonly string OrleansClusterId = "squidex-v2";
public static readonly string ApiSecurityScheme = "custom";
public static readonly string ApiSecurityScheme = "API";
public static readonly string OrleansClusterId = "squidex-v2";
public static readonly string PrefixApi = "/api";
public static readonly string OrleansPrefix = "/orleans";
public static readonly string PrefixOrleans = "/orleans";
public static readonly string PortalPrefix = "/portal";
public static readonly string PrefixPortal = "/portal";
public static readonly string EmailScope = "email";
public static readonly string PrefixIdentityServer = "/identity-server";
public static readonly string RoleScope = "role";
public static readonly string ScopePermissions = "permissions";
public static readonly string PermissionsScope = "permissions";
public static readonly string ScopeProfile = "squidex-profile";
public static readonly string ProfileScope = "squidex-profile";
public static readonly string ScopeRole = "role";
public static readonly string FrontendClient = DefaultClients.Frontend;
public static readonly string ScopeApi = "squidex-api";
public static readonly string InternalClientId = "squidex-internal";
public static readonly string ClientFrontendId = DefaultClients.Frontend;
public static readonly string InternalClientSecret = "squidex-internal".ToSha256Base64();
public static readonly string ClientInternalId = "squidex-internal";
public static readonly string IdentityServerPrefix = "/identity-server";
public static readonly string ClientInternalSecret = "squidex-internal".ToSha256Base64();
}
}

31
backend/src/Squidex/Areas/Api/Config/IdentityServerPathMiddleware.cs

@ -1,31 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using Microsoft.AspNetCore.Http;
using Squidex.Web;
namespace Squidex.Areas.Api.Config
{
public sealed class IdentityServerPathMiddleware
{
private readonly RequestDelegate next;
public IdentityServerPathMiddleware(RequestDelegate next)
{
this.next = next;
}
public Task InvokeAsync(HttpContext context)
{
context.SetIdentityServerBasePath(Constants.IdentityServerPrefix);
return next(context);
}
}
}

2
backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs

@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
public void Process(DocumentProcessorContext context)
{
context.Document.BasePath = Constants.ApiPrefix;
context.Document.BasePath = Constants.PrefixApi;
context.Document.Info.Version = version;
context.Document.Info.ExtensionData = new Dictionary<string, object>

4
backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs

@ -28,7 +28,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
Type = OpenApiSecuritySchemeType.OAuth2
};
var tokenUrl = urlGenerator.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token", false);
var tokenUrl = urlGenerator.BuildUrl($"{Constants.PrefixIdentityServer}/connect/token", false);
security.TokenUrl = tokenUrl;
@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
{
security.Scopes = new Dictionary<string, string>
{
[Constants.ApiScope] = "Read and write access to the API"
[Constants.ScopeApi] = "Read and write access to the API"
};
}

5
backend/src/Squidex/Areas/Api/Startup.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.AspNetCore.Builder;
using Squidex.Areas.Api.Config;
using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Web;
using Squidex.Web.Pipeline;
@ -17,10 +16,8 @@ namespace Squidex.Areas.Api
{
public static void ConfigureApi(this IApplicationBuilder app)
{
app.Map(Constants.ApiPrefix, appApi =>
app.Map(Constants.PrefixApi, appApi =>
{
appApi.UseMiddleware<IdentityServerPathMiddleware>();
appApi.UseAccessTokenQueryString();
appApi.UseRouting();

59
backend/src/Squidex/Areas/IdentityServer/Config/ApplicationExtensions.cs

@ -0,0 +1,59 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using OpenIddict.Abstractions;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config
{
public static class ApplicationExtensions
{
public static OpenIddictApplicationDescriptor SetAdmin(this OpenIddictApplicationDescriptor application)
{
application.Properties[SquidexClaimTypes.Permissions] = CreateParameter(Enumerable.Repeat(Permissions.All, 1));
return application;
}
public static OpenIddictApplicationDescriptor CopyClaims(this OpenIddictApplicationDescriptor application, IUser claims)
{
foreach (var group in claims.Claims.GroupBy(x => x.Type))
{
application.Properties[group.Key] = CreateParameter(group.Select(x => x.Value));
}
return application;
}
private static JsonElement CreateParameter(IEnumerable<string> values)
{
return (JsonElement)new OpenIddictParameter(values.ToArray());
}
public static IEnumerable<Claim> Claims(this IReadOnlyDictionary<string, JsonElement> properties)
{
foreach (var (key, value) in properties)
{
var values = (string[]?)new OpenIddictParameter(value);
if (values != null)
{
foreach (var claimValue in values)
{
yield return new Claim(key, claimValue);
}
}
}
}
}
}

33
backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Core;
namespace Squidex.Areas.IdentityServer.Config
{
public sealed class ApplicationManager<T> : OpenIddictApplicationManager<T> where T : class
{
public ApplicationManager(
IOptionsMonitor<OpenIddictCoreOptions> options,
IOpenIddictApplicationCache<T> cache,
IOpenIddictApplicationStoreResolver resolver,
ILogger<OpenIddictApplicationManager<T>> logger)
: base(cache, logger, options, resolver)
{
}
protected override ValueTask<bool> ValidateClientSecretAsync(string secret, string comparand, CancellationToken cancellationToken = default)
{
return new ValueTask<bool>(string.Equals(secret, comparand));
}
}
}

243
backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs

@ -0,0 +1,243 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Users;
using Squidex.Domain.Users.InMemory;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
using Squidex.Web;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Squidex.Areas.IdentityServer.Config
{
public class DynamicApplicationStore : InMemoryApplicationStore
{
private readonly IServiceProvider serviceProvider;
public DynamicApplicationStore(IServiceProvider serviceProvider)
: base(CreateStaticClients(serviceProvider))
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}
public override async ValueTask<ImmutableApplication?> FindByIdAsync(string identifier, CancellationToken cancellationToken)
{
var application = await base.FindByIdAsync(identifier, cancellationToken);
if (application == null)
{
application = await GetDynamicAsync(identifier);
}
return application;
}
public override async ValueTask<ImmutableApplication?> FindByClientIdAsync(string identifier, CancellationToken cancellationToken)
{
var application = await base.FindByClientIdAsync(identifier, cancellationToken);
if (application == null)
{
application = await GetDynamicAsync(identifier);
}
return application;
}
private async Task<ImmutableApplication?> GetDynamicAsync(string clientId)
{
var (appName, appClientId) = clientId.GetClientParts();
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId))
{
var app = await appProvider.GetAppAsync(appName, true);
var appClient = app?.Clients.GetOrDefault(appClientId);
if (appClient != null)
{
return CreateClientFromApp(clientId, appClient);
}
}
using (var scope = serviceProvider.CreateScope())
{
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
var user = await userService.FindByIdAsync(clientId);
if (user == null)
{
return null;
}
var secret = user.Claims.ClientSecret();
if (!string.IsNullOrWhiteSpace(secret))
{
return CreateClientFromUser(user, secret);
}
}
return null;
}
private static ImmutableApplication CreateClientFromUser(IUser user, string secret)
{
return new ImmutableApplication(user.Id, new OpenIddictApplicationDescriptor
{
DisplayName = $"{user.Email} Client",
ClientId = user.Id,
ClientSecret = secret,
Permissions =
{
Permissions.Endpoints.Token,
Permissions.GrantTypes.ClientCredentials,
Permissions.ResponseTypes.Token,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + Constants.ScopeApi,
Permissions.Prefixes.Scope + Constants.ScopePermissions
}
}.CopyClaims(user));
}
private static ImmutableApplication CreateClientFromApp(string id, AppClient appClient)
{
return new ImmutableApplication(id, new OpenIddictApplicationDescriptor
{
DisplayName = id,
ClientId = id,
ClientSecret = appClient.Secret,
Permissions =
{
Permissions.Endpoints.Token,
Permissions.GrantTypes.ClientCredentials,
Permissions.ResponseTypes.Token,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + Constants.ScopeApi,
Permissions.Prefixes.Scope + Constants.ScopePermissions
}
});
}
private static IEnumerable<(string, OpenIddictApplicationDescriptor)> CreateStaticClients(IServiceProvider serviceProvider)
{
var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value;
var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>();
var frontendId = Constants.ClientFrontendId;
yield return (frontendId, new OpenIddictApplicationDescriptor
{
DisplayName = "Frontend Client",
ClientId = frontendId,
ClientSecret = null,
RedirectUris =
{
new Uri(urlGenerator.BuildUrl("login;")),
new Uri(urlGenerator.BuildUrl("client-callback-silent", false)),
new Uri(urlGenerator.BuildUrl("client-callback-popup", false))
},
PostLogoutRedirectUris =
{
new Uri(urlGenerator.BuildUrl("logout", false))
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + Constants.ScopeApi,
Permissions.Prefixes.Scope + Constants.ScopePermissions
},
Type = ClientTypes.Public
});
var internalClientId = Constants.ClientInternalId;
yield return (internalClientId, new OpenIddictApplicationDescriptor
{
DisplayName = "Internal Client",
ClientId = internalClientId,
ClientSecret = Constants.ClientInternalSecret,
RedirectUris =
{
new Uri(urlGenerator.BuildUrl($"{Constants.PrefixPortal}/signin-internal", false)),
new Uri(urlGenerator.BuildUrl($"{Constants.PrefixOrleans}/signin-internal", false))
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.Implicit,
Permissions.ResponseTypes.IdToken,
Permissions.ResponseTypes.IdTokenToken,
Permissions.ResponseTypes.Token,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + Constants.ScopeApi,
Permissions.Prefixes.Scope + Constants.ScopePermissions
},
Type = ClientTypes.Public
});
if (!identityOptions.IsAdminClientConfigured())
{
yield break;
}
var adminClientId = identityOptions.AdminClientId;
yield return (adminClientId, new OpenIddictApplicationDescriptor
{
DisplayName = "Admin Client",
ClientId = adminClientId,
ClientSecret = identityOptions.AdminClientSecret,
Permissions =
{
Permissions.Endpoints.Token,
Permissions.GrantTypes.ClientCredentials,
Permissions.ResponseTypes.Token,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + Constants.ScopeApi,
Permissions.Prefixes.Scope + Constants.ScopePermissions
}
}.SetAdmin());
}
}
}

38
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerConfiguration.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using OpenIddict.Abstractions;
using Squidex.Domain.Users.InMemory;
using Squidex.Web;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Squidex.Areas.IdentityServer.Config
{
public static class IdentityServerConfiguration
{
public sealed class Scopes : InMemoryScopeStore
{
public Scopes()
: base(BuildScopes())
{
}
private static IEnumerable<(string, OpenIddictScopeDescriptor)> BuildScopes()
{
yield return (Constants.ScopeApi, new OpenIddictScopeDescriptor
{
Name = Constants.ScopeApi,
Resources =
{
Permissions.Prefixes.Scope + Constants.ScopeApi
}
});
}
}
}
}

119
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -5,20 +5,21 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using Squidex.Domain.Users;
using Squidex.Shared.Identity;
using Squidex.Domain.Users.InMemory;
using Squidex.Hosting;
using Squidex.Web;
using Squidex.Web.Pipeline;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Squidex.Areas.IdentityServer.Config
{
@ -40,74 +41,86 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddSingletonAs<DefaultXmlRepository>()
.As<IXmlRepository>();
services.AddSingletonAs<DefaultKeyStore>()
.As<ISigningCredentialStore>().As<IValidationKeysStore>();
services.AddSingletonAs<PwnedPasswordValidator>()
.As<IPasswordValidator<IdentityUser>>();
services.AddScopedAs<DefaultUserService>()
.As<IUserService>();
services.AddSingletonAs<PwnedPasswordValidator>()
.As<IPasswordValidator<IdentityUser>>();
services.AddScopedAs<UserClaimsPrincipalFactoryWithEmail>()
.As<IUserClaimsPrincipalFactory<IdentityUser>>();
services.AddSingletonAs<ApiPermissionUnifier>()
.As<IClaimsTransformation>();
services.AddSingletonAs<LazyClientStore>()
.As<IClientStore>();
services.AddSingletonAs<InMemoryResourcesStore>()
.As<IResourceStore>();
services.AddSingletonAs<CreateAdminInitializer>()
.AsSelf();
services.AddIdentityServer(options =>
{
options.UserInteraction.ErrorUrl = "/error/";
})
.AddAspNetIdentity<IdentityUser>()
.AddInMemoryApiScopes(GetApiScopes())
.AddInMemoryIdentityResources(GetIdentityResources());
}
services.ConfigureOptions<DefaultKeyStore>();
private static IEnumerable<ApiScope> GetApiScopes()
{
yield return new ApiScope(Constants.ApiScope)
services.Configure<IdentityOptions>(options =>
{
UserClaims = new List<string>
{
JwtClaimTypes.Email,
JwtClaimTypes.Role,
SquidexClaimTypes.Permissions
}
};
}
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
private static IEnumerable<IdentityResource> GetIdentityResources()
{
yield return new IdentityResources.OpenId();
yield return new IdentityResources.Profile();
yield return new IdentityResources.Email();
yield return new IdentityResource(Constants.RoleScope,
new[]
services.AddOpenIddict()
.AddCore(builder =>
{
JwtClaimTypes.Role
});
yield return new IdentityResource(Constants.PermissionsScope,
new[]
builder.Services.AddSingletonAs<IdentityServerConfiguration.Scopes>()
.As<IOpenIddictScopeStore<ImmutableScope>>();
builder.Services.AddSingletonAs<DynamicApplicationStore>()
.As<IOpenIddictApplicationStore<ImmutableApplication>>();
builder.ReplaceApplicationManager(typeof(ApplicationManager<>));
})
.AddServer(builder =>
{
SquidexClaimTypes.Permissions
});
yield return new IdentityResource(Constants.ProfileScope,
new[]
builder
.SetAuthorizationEndpointUris("/connect/authorize")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo");
builder.DisableAccessTokenEncryption();
builder.RegisterScopes(
Scopes.Email,
Scopes.Profile,
Scopes.Roles,
Constants.ScopeApi,
Constants.ScopePermissions);
builder.SetAccessTokenLifetime(TimeSpan.FromDays(30));
builder.AllowClientCredentialsFlow();
builder.AllowImplicitFlow();
builder.AllowAuthorizationCodeFlow();
builder.UseAspNetCore()
// Disable it mainly for our tests.
.DisableTransportSecurityRequirement()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
})
.AddValidation(options =>
{
SquidexClaimTypes.DisplayName,
SquidexClaimTypes.PictureUrl,
SquidexClaimTypes.NotifoKey
options.UseLocalServer();
options.UseAspNetCore();
});
services.Configure<OpenIddictServerOptions>((services, options) =>
{
var urlGenerator = services.GetRequiredService<IUrlGenerator>();
options.Issuer = new Uri(urlGenerator.BuildUrl("/identity-server", false));
});
}
}
}

253
backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -1,253 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Users;
using Squidex.Hosting;
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 IServiceProvider serviceProvider;
private readonly Dictionary<string, Client> staticClients = new Dictionary<string, Client>(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
CreateStaticClients();
}
public async Task<Client?> FindClientByIdAsync(string clientId)
{
var client = staticClients.GetOrDefault(clientId);
if (client != null)
{
return client;
}
var (appName, appClientId) = clientId.GetClientParts();
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
if (!string.IsNullOrWhiteSpace(appName) && !string.IsNullOrWhiteSpace(appClientId))
{
var app = await appProvider.GetAppAsync(appName, true);
var appClient = app?.Clients.GetOrDefault(appClientId);
if (appClient != null)
{
return CreateClientFromApp(clientId, appClient);
}
}
using (var scope = serviceProvider.CreateScope())
{
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
var user = await userService.FindByIdAsync(clientId);
if (user == null)
{
return null;
}
var secret = user.Claims.ClientSecret();
if (!string.IsNullOrWhiteSpace(secret))
{
return CreateClientFromUser(user, secret);
}
}
return null;
}
private static Client CreateClientFromUser(IUser user, string secret)
{
return new Client
{
ClientId = user.Id,
ClientName = $"{user.Email} Client",
ClientClaimsPrefix = null,
ClientSecrets = new List<Secret>
{
new Secret(secret.Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = GetClaims(user)
};
}
private static Client CreateClientFromApp(string id, AppClient appClient)
{
return new Client
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret>
{
new Secret(appClient.Secret.Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
}
};
}
private void CreateStaticClients()
{
var identityOptions = serviceProvider.GetRequiredService<IOptions<MyIdentityOptions>>().Value;
var urlGenerator = serviceProvider.GetRequiredService<IUrlGenerator>();
foreach (var client in CreateStaticClients(urlGenerator, identityOptions))
{
staticClients[client.ClientId] = client;
}
}
private static IEnumerable<Client> CreateStaticClients(IUrlGenerator urlGenerator, MyIdentityOptions identityOptions)
{
var frontendId = Constants.FrontendClient;
yield return new Client
{
ClientId = frontendId,
ClientName = frontendId,
RedirectUris = new List<string>
{
urlGenerator.BuildUrl("login;"),
urlGenerator.BuildUrl("client-callback-silent", false),
urlGenerator.BuildUrl("client-callback-popup", false)
},
PostLogoutRedirectUris = new List<string>
{
urlGenerator.BuildUrl("logout", false)
},
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
Constants.ApiScope,
Constants.PermissionsScope,
Constants.ProfileScope,
Constants.RoleScope
},
RequireConsent = false
};
var internalClient = Constants.InternalClientId;
yield return new Client
{
ClientId = internalClient,
ClientName = internalClient,
ClientSecrets = new List<Secret>
{
new Secret(Constants.InternalClientSecret)
},
RedirectUris = new List<string>
{
urlGenerator.BuildUrl($"{Constants.PortalPrefix}/signin-internal", false),
urlGenerator.BuildUrl($"{Constants.OrleansPrefix}/signin-internal", false)
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
Constants.ApiScope,
Constants.PermissionsScope,
Constants.ProfileScope,
Constants.RoleScope
},
RequireConsent = false
};
if (identityOptions.IsAdminClientConfigured())
{
var id = identityOptions.AdminClientId;
yield return new Client
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret>
{
new Secret(identityOptions.AdminClientSecret.Sha256())
},
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>
{
Constants.ApiScope,
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<ClientClaim>
{
new ClientClaim(SquidexClaimTypes.Permissions, Permissions.All)
}
};
}
}
private static List<ClientClaim> GetClaims(IUser user)
{
var claims = new List<ClientClaim>
{
new ClientClaim(OpenIdClaims.Subject, user.Id)
};
claims.AddRange(
user.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions)
.Select(x => new ClientClaim(x.Type, x.Value)));
return claims;
}
}
}

43
backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -8,11 +8,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -31,15 +26,12 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
private readonly IUserService userService;
private readonly MyIdentityOptions identityOptions;
private readonly IIdentityServerInteractionService interactions;
public AccountController(
IUserService userService,
IOptions<MyIdentityOptions> identityOptions,
IIdentityServerInteractionService interactions)
IOptions<MyIdentityOptions> identityOptions)
{
this.identityOptions = identityOptions.Value;
this.interactions = interactions;
this.userService = userService;
}
@ -130,24 +122,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
await SignInManager.SignOutAsync();
if (User.Identity?.IsAuthenticated == true)
{
var provider = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (provider != null && provider != IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(provider);
if (providerSupportsSignout)
{
return SignOut(provider);
}
}
}
var context = await interactions.GetLogoutContextAsync(logoutId);
return RedirectToLogoutUrl(context);
return Redirect("~/../");
}
[HttpGet]
@ -341,17 +316,5 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return (result.Succeeded, result.IsLockedOut);
}
private IActionResult RedirectToLogoutUrl(LogoutRequest context)
{
if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri))
{
return Redirect(context.PostLogoutRedirectUri);
}
else
{
return Redirect("~/../");
}
}
}
}

287
backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs

@ -0,0 +1,287 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using Squidex.Areas.IdentityServer.Config;
using Squidex.Areas.IdentityServer.Controllers;
using Squidex.Domain.Users;
using Squidex.Shared.Identity;
using Squidex.Web;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Notifo.Areas.Account.Controllers
{
public class AuthorizationController : IdentityServerController
{
private readonly IOpenIddictScopeManager scopeManager;
private readonly IOpenIddictApplicationManager applicationManager;
private readonly IUserService userService;
public AuthorizationController(
IOpenIddictScopeManager scopeManager,
IOpenIddictApplicationManager applicationManager,
IUserService userService)
{
this.scopeManager = scopeManager;
this.applicationManager = applicationManager;
this.userService = userService;
}
[HttpPost("connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
{
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType() || request.IsImplicitFlow())
{
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
if (principal == null)
{
throw new InvalidOperationException("The user details cannot be retrieved.");
}
var user = await userService.GetAsync(principal);
if (user == null)
{
return Forbid(
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}),
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (!await SignInManager.CanSignInAsync((IdentityUser)user.Identity))
{
return Forbid(
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}),
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal, false));
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsClientCredentialsGrantType())
{
if (request.ClientId == null)
{
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
var application = await applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
var principal = await CreateApplicationPrinicpalAsync(request, application);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
[HttpGet("connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
{
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
if (User.Identity?.IsAuthenticated != true)
{
if (request.HasPrompt(Prompts.None))
{
var properties = new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var query = QueryString.Create(
Request.HasFormContentType ?
Request.Form.ToList() :
Request.Query.ToList());
var redirectUri = Request.PathBase + Request.Path + query;
return Challenge(
new AuthenticationProperties
{
RedirectUri = redirectUri
});
}
var user = await userService.GetAsync(User);
if (user == null)
{
throw new InvalidOperationException("The user details cannot be retrieved.");
}
var principal = await SignInManager.CreateUserPrincipalAsync((IdentityUser)user.Identity);
await EnrichPrincipalAsync(request, principal, false);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[HttpGet("connect/logout")]
public async Task<IActionResult> Logout()
{
await SignInManager.SignOutAsync();
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
private async Task<ClaimsPrincipal> CreateApplicationPrinicpalAsync(OpenIddictRequest request, object application)
{
var identity = new ClaimsIdentity(
TokenValidationParameters.DefaultAuthenticationType,
Claims.Name,
Claims.Role);
var principal = new ClaimsPrincipal(identity);
var clientId = request.ClientId;
var clientName = await applicationManager.GetDisplayNameAsync(application);
if (clientId != null)
{
identity.AddClaim(Claims.Subject, clientId,
Destinations.AccessToken, Destinations.IdentityToken);
}
if (clientName != null)
{
identity.AddClaim(Claims.Name, clientName,
Destinations.AccessToken, Destinations.IdentityToken);
}
var properties = await applicationManager.GetPropertiesAsync(application);
foreach (var claim in properties.Claims())
{
identity.AddClaim(claim);
}
await EnrichPrincipalAsync(request, principal, true);
return principal;
}
private async Task EnrichPrincipalAsync(OpenIddictRequest request, ClaimsPrincipal principal, bool alwaysDeliverPermissions)
{
var scopes = request.GetScopes();
principal.SetScopes(scopes);
principal.SetResources(await scopeManager.ListResourcesAsync(scopes).ToListAsync());
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal, alwaysDeliverPermissions));
}
}
private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal, bool alwaysDeliverPermissions)
{
switch (claim.Type)
{
case SquidexClaimTypes.DisplayName when principal.HasScope(Scopes.Profile):
yield return Destinations.IdentityToken;
yield break;
case SquidexClaimTypes.PictureUrl when principal.HasScope(Scopes.Profile):
yield return Destinations.IdentityToken;
yield break;
case SquidexClaimTypes.NotifoKey when principal.HasScope(Scopes.Profile):
yield return Destinations.IdentityToken;
yield break;
case SquidexClaimTypes.Permissions when principal.HasScope(Constants.ScopePermissions) || alwaysDeliverPermissions:
yield return Destinations.AccessToken;
yield return Destinations.IdentityToken;
yield break;
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
{
yield return Destinations.IdentityToken;
}
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
{
yield return Destinations.IdentityToken;
}
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
{
yield return Destinations.IdentityToken;
}
yield break;
case "AspNet.Identity.SecurityStamp":
yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
}

37
backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs

@ -6,23 +6,14 @@
// ==========================================================================
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure;
namespace Squidex.Areas.IdentityServer.Controllers.Error
{
public sealed class ErrorController : IdentityServerController
{
private readonly IIdentityServerInteractionService interaction;
public ErrorController(IIdentityServerInteractionService interaction)
{
this.interaction = interaction;
}
[Route("error/")]
public async Task<IActionResult> Error(string? errorId = null)
{
@ -30,28 +21,16 @@ namespace Squidex.Areas.IdentityServer.Controllers.Error
var vm = new ErrorVM();
if (!string.IsNullOrWhiteSpace(errorId))
{
var message = await interaction.GetErrorContextAsync(errorId);
var response = HttpContext.GetOpenIddictServerResponse();
if (message != null)
{
vm.Error = message;
}
}
vm.ErrorMessage = response?.ErrorDescription;
vm.ErrorCode = response?.Error;
if (vm.Error == null)
if (string.IsNullOrWhiteSpace(vm.ErrorMessage))
{
var error = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error;
if (error is DomainException exception)
{
vm.Error = new ErrorMessage { ErrorDescription = exception.Message };
}
else if (error?.InnerException is DomainException exception2)
{
vm.Error = new ErrorMessage { ErrorDescription = exception2.Message };
}
var exception = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error;
vm.ErrorMessage = exception?.Message;
}
return View("Error", vm);

6
backend/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorVM.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using IdentityServer4.Models;
namespace Squidex.Areas.IdentityServer.Controllers.Error
{
public class ErrorVM
{
public ErrorMessage Error { get; set; }
public string? ErrorMessage { get; set; }
public string? ErrorCode { get; set; } = "400";
}
}

13
backend/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
@ -20,10 +19,7 @@ namespace Squidex.Areas.IdentityServer.Controllers
{
public SignInManager<IdentityUser> SignInManager
{
get
{
return HttpContext.RequestServices.GetRequiredService<SignInManager<IdentityUser>>();
}
get => HttpContext.RequestServices.GetRequiredService<SignInManager<IdentityUser>>();
}
public override void OnActionExecuting(ActionExecutingContext context)
@ -50,13 +46,6 @@ namespace Squidex.Areas.IdentityServer.Controllers
return Redirect(returnUrl);
}
var interactions = HttpContext.RequestServices.GetRequiredService<IIdentityServerInteractionService>();
if (interactions.IsValidReturnUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/../");
}
}

68
backend/src/Squidex/Areas/IdentityServer/Controllers/UserInfo/UserInfoController.cs

@ -0,0 +1,68 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using Squidex.Domain.Users;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Squidex.Areas.IdentityServer.Controllers.UserInfo
{
public class UserInfoController : IdentityServerController
{
private readonly IUserService userService;
public UserInfoController(IUserService userService)
{
this.userService = userService;
}
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("connect/userinfo")]
[HttpPost("connect/userinfo")]
[Produces("application/json")]
public async Task<IActionResult> UserInfo()
{
var user = await userService.GetAsync(User);
if (user == null)
{
return Challenge(
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified access token is bound to an account that no longer exists."
}),
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[Claims.Subject] = user.Id
};
if (User.HasScope(Scopes.Email))
{
claims[Claims.Email] = user.Email;
claims[Claims.EmailVerified] = true;
}
if (User.HasScope(Scopes.Roles))
{
claims[Claims.Role] = Array.Empty<string>();
}
return Ok(claims);
}
}
}

13
backend/src/Squidex/Areas/IdentityServer/Startup.cs

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Squidex.Areas.IdentityServer.Config;
using Squidex.Web;
namespace Squidex.Areas.IdentityServer
@ -20,7 +19,7 @@ namespace Squidex.Areas.IdentityServer
{
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
app.Map(Constants.IdentityServerPrefix, identityApp =>
app.Map(Constants.PrefixIdentityServer, identityApp =>
{
if (environment.IsDevelopment())
{
@ -31,13 +30,19 @@ namespace Squidex.Areas.IdentityServer
identityApp.UseExceptionHandler("/error");
}
identityApp.Use((context, next) =>
{
// OpenId dict core only works with https, which sucks in our tests.
context.Request.IsHttps = true;
return next();
});
identityApp.UseRouting();
identityApp.UseAuthentication();
identityApp.UseAuthorization();
identityApp.UseSquidexIdentityServer();
identityApp.UseEndpoints(endpoints =>
{
endpoints.MapControllers();

4
backend/src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml

@ -11,9 +11,9 @@
<h1 class="splash-h1">@T.Get("users.error.headline")</h1>
<p class="splash-text">
@if (Model.Error?.ErrorDescription != null)
@if (Model.ErrorMessage != null)
{
@Model.Error.ErrorDescription
@Model.ErrorMessage
}
else
{

2
backend/src/Squidex/Areas/OrleansDashboard/Startup.cs

@ -16,7 +16,7 @@ namespace Squidex.Areas.OrleansDashboard
{
public static void ConfigureOrleansDashboard(this IApplicationBuilder app)
{
app.Map(Constants.OrleansPrefix, orleansApp =>
app.Map(Constants.PrefixOrleans, orleansApp =>
{
orleansApp.UseAuthentication();
orleansApp.UseAuthorization();

2
backend/src/Squidex/Areas/Portal/Startup.cs

@ -15,7 +15,7 @@ namespace Squidex.Areas.Portal
{
public static void ConfigurePortal(this IApplicationBuilder app)
{
app.Map(Constants.PortalPrefix, portalApp =>
app.Map(Constants.PrefixPortal, portalApp =>
{
portalApp.UseAuthentication();
portalApp.UseAuthorization();

61
backend/src/Squidex/Config/Authentication/IdentityServerServices.cs

@ -5,17 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using IdentityServer4;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Hosting.LocalApiAuthentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Validation.AspNetCore;
using Squidex.Hosting;
using Squidex.Web;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Squidex.Config.Authentication
{
@ -27,26 +25,28 @@ namespace Squidex.Config.Authentication
if (useCustomAuthorityUrl)
{
authBuilder.AddIdentityServerAuthentication(options =>
const string ExternalIdentityServerSchema = nameof(ExternalIdentityServerSchema);
authBuilder.AddOpenIdConnect(ExternalIdentityServerSchema, options =>
{
options.Authority = identityOptions.AuthorityUrl;
options.ApiName = Constants.ApiScope;
options.ApiSecret = null;
options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SupportedTokens = SupportedTokens.Jwt;
options.Scope.Add(Scopes.Email);
options.Scope.Add(Scopes.Profile);
options.Scope.Add(Constants.ScopePermissions);
options.Scope.Add(Constants.ScopeApi);
});
authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options =>
{
options.ForwardDefaultSelector = context => ExternalIdentityServerSchema;
});
}
else
{
authBuilder.AddLocalApi();
authBuilder.Services.AddOptions<LocalApiAuthenticationOptions>(IdentityServerConstants.LocalApi.PolicyName)
.Configure<IUrlGenerator>((options, urlGenerator) =>
{
options.ClaimsIssuer = urlGenerator.BuildUrl(Constants.IdentityServerPrefix, false);
options.ExpectedScope = Constants.ApiScope;
});
authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options =>
{
options.ForwardDefaultSelector = _ => OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
}
authBuilder.AddOpenIdConnect();
@ -60,33 +60,20 @@ namespace Squidex.Config.Authentication
}
else
{
options.Authority = urlGenerator.BuildUrl(Constants.IdentityServerPrefix, false);
options.Authority = urlGenerator.BuildUrl(Constants.PrefixIdentityServer, false);
}
options.ClientId = Constants.InternalClientId;
options.ClientSecret = Constants.InternalClientSecret;
options.ClientId = Constants.ClientInternalId;
options.ClientSecret = Constants.ClientInternalSecret;
options.CallbackPath = "/signin-internal";
options.RequireHttpsMetadata = identityOptions.RequiresHttps;
options.SaveTokens = true;
options.Scope.Add(Constants.PermissionsScope);
options.Scope.Add(Constants.ProfileScope);
options.Scope.Add(Constants.RoleScope);
options.Scope.Add(Scopes.Email);
options.Scope.Add(Scopes.Profile);
options.Scope.Add(Constants.ScopePermissions);
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
authBuilder.AddPolicyScheme(Constants.ApiSecurityScheme, Constants.ApiSecurityScheme, options =>
{
options.ForwardDefaultSelector = context =>
{
if (useCustomAuthorityUrl)
{
return IdentityServerAuthenticationDefaults.AuthenticationScheme;
}
return IdentityServerConstants.LocalApi.PolicyName;
};
});
return authBuilder;
}
}

1
backend/src/Squidex/Config/Authentication/OidcHandler.cs

@ -46,6 +46,7 @@ namespace Squidex.Config.Authentication
if (!string.IsNullOrEmpty(options.OidcOnSignoutRedirectUrl))
{
var logoutUri = options.OidcOnSignoutRedirectUrl;
context.Response.Redirect(logoutUri);
context.HandleResponse();

5
backend/src/Squidex/Config/Domain/LoggingServices.cs

@ -74,6 +74,11 @@ namespace Squidex.Config.Domain
return false;
}
if (category.StartsWith("OpenIddict", StringComparison.OrdinalIgnoreCase))
{
return level >= LogLevel.Warning;
}
if (category.StartsWith("Orleans.Runtime.NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase))
{
return level >= LogLevel.Error;

19
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -6,8 +6,6 @@
// ==========================================================================
using System;
using System.Linq;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -29,8 +27,8 @@ using Squidex.Domain.Apps.Entities.MongoDb.Schemas;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Users;
using Squidex.Domain.Users.InMemory;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing;
@ -127,13 +125,16 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<MongoTextIndexerState>()
.As<ITextIndexerState>();
var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore));
services.AddOpenIddict()
.AddCore(builder =>
{
builder.UseMongoDb<string>()
.SetScopesCollectionName("Identity_Scopes")
.SetTokensCollectionName("Identity_Tokens");
if (registration == null || registration.ImplementationType == typeof(InMemoryPersistedGrantStore))
{
services.AddSingletonAs<MongoPersistedGrantStore>()
.As<IPersistedGrantStore>();
}
builder.SetDefaultScopeEntity<ImmutableScope>();
builder.SetDefaultApplicationEntity<ImmutableApplication>();
});
}
});

5
backend/src/Squidex/Squidex.csproj

@ -37,11 +37,9 @@
<PackageReference Include="GraphQL.Server.Core" Version="5.0.1" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.0.1" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.0.1" />
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.5" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" />
@ -70,6 +68,7 @@
<PackageReference Include="Squidex.Caching.Orleans" Version="1.8.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="6.19.0" />
<PackageReference Include="Squidex.Hosting" Version="1.9.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />

64
backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs

@ -5,8 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using System.Security.Cryptography;
using FakeItEasy;
using IdentityModel;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Xunit;
@ -24,39 +27,72 @@ namespace Squidex.Domain.Users
}
[Fact]
public async Task Should_generate_signing_credentials_once()
public void Should_configure_new_keys()
{
A.CallTo(() => store.ReadAsync(A<DomainId>._))
.Returns((null!, true, 0));
var credentials1 = await sut.GetSigningCredentialsAsync();
var credentials2 = await sut.GetSigningCredentialsAsync();
var options = new OpenIddictServerOptions();
Assert.Same(credentials1, credentials2);
sut.Configure(options);
A.CallTo(() => store.ReadAsync(A<DomainId>._))
.MustHaveHappenedOnceExactly();
Assert.NotEmpty(options.SigningCredentials);
Assert.NotEmpty(options.EncryptionCredentials);
A.CallTo(() => store.WriteAsync(A<DomainId>._, A<DefaultKeyStore.State>._, 0, 0))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_generate_validation_keys_once()
public void Should_configure_existing_keys()
{
A.CallTo(() => store.ReadAsync(A<DomainId>._))
.Returns((null!, true, 0));
.Returns((ExistingKey(), true, 0));
var options = new OpenIddictServerOptions();
sut.Configure(options);
var credentials1 = await sut.GetValidationKeysAsync();
var credentials2 = await sut.GetValidationKeysAsync();
Assert.NotEmpty(options.SigningCredentials);
Assert.NotEmpty(options.EncryptionCredentials);
Assert.Same(credentials1, credentials2);
A.CallTo(() => store.WriteAsync(A<DomainId>._, A<DefaultKeyStore.State>._, 0, 0))
.MustNotHaveHappened();
}
[Fact]
public void Should_configure_existing_keys_when_initial_setup_failed()
{
A.CallTo(() => store.ReadAsync(A<DomainId>._))
.MustHaveHappenedOnceExactly();
.Returns((null!, true, 0)).Once()
.Then
.Returns((ExistingKey(), true, 0));
A.CallTo(() => store.WriteAsync(A<DomainId>._, A<DefaultKeyStore.State>._, 0, 0))
.MustHaveHappenedOnceExactly();
.Throws(new InconsistentStateException(0, 0));
var options = new OpenIddictServerOptions();
sut.Configure(options);
Assert.NotEmpty(options.SigningCredentials);
Assert.NotEmpty(options.EncryptionCredentials);
A.CallTo(() => store.WriteAsync(A<DomainId>._, A<DefaultKeyStore.State>._, 0, 0))
.MustHaveHappened();
}
private static DefaultKeyStore.State ExistingKey()
{
var key = new RsaSecurityKey(RSA.Create(2048))
{
KeyId = CryptoRandom.CreateUniqueId(16)
};
return new DefaultKeyStore.State
{
Parameters = key.Rsa.ExportParameters(includePrivateParameters: true)
};
}
}
}

2
backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs

@ -51,7 +51,7 @@ namespace TestSuite.ApiTests
// STEP 3: Check contributors
var contributors = await _.Apps.GetContributorsAsync(appName);
// Should not client itself as a contributor.
// Should not add client itself as a contributor.
Assert.Empty(contributors.Items);

4
frontend/app/shared/services/auth.service.ts

@ -77,8 +77,8 @@ export class AuthService {
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile email squidex-profile role permissions',
response_type: 'id_token token',
scope: 'squidex-api openid profile email permissions',
response_type: 'code',
redirect_uri: apiUrl.buildUrl('login;'),
post_logout_redirect_uri: apiUrl.buildUrl('logout'),
silent_redirect_uri: apiUrl.buildUrl('client-callback-silent'),

44
frontend/package-lock.json

@ -1299,6 +1299,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@ -1809,7 +1810,8 @@
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"body-parser": {
"version": "1.19.0",
@ -1884,6 +1886,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
@ -2066,6 +2069,7 @@
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
@ -3313,9 +3317,9 @@
}
},
"detect-node": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.5.tgz",
"integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true
},
"di": {
@ -3958,9 +3962,9 @@
}
},
"eslint-plugin-import": {
"version": "2.23.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.1.tgz",
"integrity": "sha512-epW62znqcFCyQeixVrqy26WpdN1Y3LZH5G9XCuiiTCVuksjC4Je+4o1z5mIpa6P1KMyz1n4RT436VSrZoA5+5A==",
"version": "2.23.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.2.tgz",
"integrity": "sha512-LmNoRptHBxOP+nb0PIKz1y6OSzCJlB+0g0IGS3XV4KaKk2q4szqQ6s6F1utVf5ZRkxk/QOTjdxe7v4VjS99Bsg==",
"dev": true,
"requires": {
"array-includes": "^3.1.3",
@ -4565,6 +4569,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
@ -4699,6 +4704,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
@ -4780,6 +4786,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
@ -5768,6 +5775,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
@ -5883,7 +5891,8 @@
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-finite": {
"version": "1.1.0",
@ -5905,6 +5914,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
@ -5937,7 +5947,8 @@
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"is-number-object": {
"version": "1.0.5",
@ -7241,7 +7252,8 @@
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"normalize-range": {
"version": "0.1.2",
@ -8426,7 +8438,8 @@
"picomatch": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz",
"integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg=="
"integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==",
"dev": true
},
"pify": {
"version": "3.0.0",
@ -10363,6 +10376,7 @@
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
@ -10818,6 +10832,7 @@
"version": "1.32.13",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.13.tgz",
"integrity": "sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
}
@ -11813,9 +11828,9 @@
}
},
"spdx-license-ids": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.8.tgz",
"integrity": "sha512-NDgA96EnaLSvtbM7trJj+t1LUR3pirkDCcz9nOUlPb5DMBGsH7oES6C3hs3j7R9oHEa1EMvReS/BUAIT5Tcr0g==",
"dev": true
},
"spdy": {
@ -12287,6 +12302,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}

2
frontend/package.json

@ -55,7 +55,6 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"rxjs": "7.0.1",
"sass": "^1.32.13",
"simplemde": "1.11.2",
"slugify": "1.5.3",
"tinymce": "5.8.0",
@ -123,6 +122,7 @@
"raw-loader": "4.0.2",
"resize-observer-polyfill": "1.5.1",
"rimraf": "3.0.2",
"sass": "^1.32.13",
"sass-lint": "1.13.1",
"sass-lint-webpack": "1.0.3",
"sass-loader": "11.1.1",

Loading…
Cancel
Save