Implement password config storage for members (#10170)

* Getting new netcore PublicAccessChecker in place

* Adds full test coverage for PublicAccessChecker

* remove PublicAccessComposer

* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller

* Implements the required methods on IMemberManager, removes old migrated code

* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops

* adds note

* adds note

* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.

* Changes name to IUmbracoEndpointBuilder

* adds note

* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect

* fixing build

* Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker

* Merges PR

* Fixes up build and notes

* Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles.

* Security stamp for members is now working

* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.

* adds note

* removes unused filter, fixes build

* fixes WebPath and tests

* Looks up entities in one query

* remove usings

* Fix test, remove stylesheet

* Set status code before we write to response to avoid error

* Ensures that users and members are validated when logging in. Shares more code between users and members.

* merge changes

* oops

* Reducing and removing published member cache

* Fixes RepositoryCacheKeys to ensure the keys are normalized

* oops didn't mean to commit this

* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy

* oops didn't mean to comit this

* bah, far out this keeps getting recommitted. sorry

* cannot inject IPublishedMemberCache and cannot have IPublishedMember

* splits out files, fixes build

* fix tests

* removes membership provider classes

* removes membership provider classes

* updates the identity map definition

* reverts commented out lines

* reverts commented out lines

* Implements members Password config in db, fixes members cookie auth to not interfere with the back office cookie auth, fixes Startup sequence, fixes startup pipeline

* commits change to Startup

* Rename migration from `MemberTableColumns2` to `AddPasswordConfigToMemberTable`

* Fix test

* Fix tests, but adding default passwordConfig to members

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Shannon Deminick
2021-04-22 23:59:13 +10:00
committed by GitHub
parent 473bc53c66
commit 39aeec0f1f
34 changed files with 483 additions and 200 deletions

View File

@@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
public bool RequireUppercase { get; set; } = false;
/// <inheritdoc/>
public string HashAlgorithmType { get; set; } = Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName;
public string HashAlgorithmType { get; set; } = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName;
/// <inheritdoc/>
public int MaxFailedAccessAttemptsBeforeLockout { get; set; } = 5;

View File

@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models.Membership
{
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Models.Membership
/// The data stored against the user for their password configuration
/// </summary>
[DataContract(Name = "userPasswordSettings", Namespace = "")]
public class UserPasswordSettings
public class PersistedPasswordSettings
{
/// <summary>
/// The algorithm name

View File

@@ -205,6 +205,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
To<ExternalLoginTableIndexes>("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}");
To<ExternalLoginTokenTable>("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}");
To<MemberTableColumns>("{1303BDCF-2295-4645-9526-2F32E8B35ABD}");
To<AddPasswordConfigToMemberTable>("{86AC839A-0D08-4D09-B7B5-027445E255A1}");
//FINAL
}

View File

@@ -0,0 +1,23 @@
using System.Linq;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
{
public class AddPasswordConfigToMemberTable : MigrationBase
{
public AddPasswordConfigToMemberTable(IMigrationContext context)
: base(context)
{
}
/// <summary>
/// Adds new columns to members table
/// </summary>
public override void Migrate()
{
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
AddColumnIfNotExists<MemberDto>(columns, "passwordConfig");
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
}
/// <summary>
/// Adds new External Login token table
/// Adds new columns to members table
/// </summary>
public override void Migrate()
{

View File

@@ -32,6 +32,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
[Constraint(Default = "''")]
public string Password { get; set; }
/// <summary>
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
/// </summary>
[Column("passwordConfig")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(500)]
public string PasswordConfig { get; set; }
[Column("securityStampToken")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(255)]

View File

@@ -127,7 +127,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
content.Id = dto.NodeId;
content.SecurityStamp = dto.SecurityStampToken;
content.EmailConfirmedDate = dto.EmailConfirmedDate;
content.PasswordConfiguration = dto.PasswordConfig;
content.Key = nodeDto.UniqueId;
content.VersionId = contentVersionDto.Id;
@@ -218,7 +218,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
SecurityStampToken = entity.SecurityStamp,
EmailConfirmedDate = entity.EmailConfirmedDate,
ContentDto = contentDto,
ContentVersionDto = BuildContentVersionDto(entity, contentDto)
ContentVersionDto = BuildContentVersionDto(entity, contentDto),
PasswordConfig = entity.PasswordConfiguration
};
return dto;
}

View File

@@ -2,10 +2,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NPoco;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PropertyEditors;
@@ -26,32 +29,67 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
/// </summary>
public class MemberRepository : ContentRepositoryBase<int, IMember, MemberRepository>, IMemberRepository
{
private readonly MemberPasswordConfigurationSettings _passwordConfiguration;
private readonly IMemberTypeRepository _memberTypeRepository;
private readonly ITagRepository _tagRepository;
private readonly IPasswordHasher _passwordHasher;
private readonly IJsonSerializer _serializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IMemberGroupRepository _memberGroupRepository;
private readonly IRepositoryCachePolicy<IMember, string> _memberByUsernameCachePolicy;
private bool _passwordConfigInitialized;
private string _passwordConfigJson;
public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger<MemberRepository> logger,
IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository,
public MemberRepository(
IScopeAccessor scopeAccessor,
AppCaches cache,
ILogger<MemberRepository> logger,
IMemberTypeRepository memberTypeRepository,
IMemberGroupRepository memberGroupRepository,
ITagRepository tagRepository,
ILanguageRepository languageRepository,
IRelationRepository relationRepository,
IRelationTypeRepository relationTypeRepository,
IPasswordHasher passwordHasher,
Lazy<PropertyEditorCollection> propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
IJsonSerializer serializer,
IEventAggregator eventAggregator)
IEventAggregator eventAggregator,
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration)
: base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator)
{
_memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository));
_tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
_passwordHasher = passwordHasher;
_serializer = serializer;
_jsonSerializer = serializer;
_memberGroupRepository = memberGroupRepository;
_passwordConfiguration = passwordConfiguration.Value;
_memberByUsernameCachePolicy = new DefaultRepositoryCachePolicy<IMember, string>(GlobalIsolatedCache, ScopeAccessor, DefaultOptions);
}
/// <summary>
/// Returns a serialized dictionary of the password configuration that is stored against the member in the database
/// </summary>
private string DefaultPasswordConfigJson
{
get
{
if (_passwordConfigInitialized)
{
return _passwordConfigJson;
}
var passwordConfig = new PersistedPasswordSettings
{
HashAlgorithm = _passwordConfiguration.HashAlgorithmType
};
_passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig);
_passwordConfigInitialized = true;
return _passwordConfigJson;
}
}
protected override MemberRepository This => this;
public override int RecycleBinId => throw new NotSupportedException();
@@ -262,17 +300,20 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
entity.SanitizeEntityPropertiesForXmlStorage();
// create the dto
var dto = ContentBaseFactory.BuildDto(entity);
MemberDto memberDto = ContentBaseFactory.BuildDto(entity);
// check if we have a user config else use the default
memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson;
// derive path and level from parent
var parent = GetParentNodeDto(entity.ParentId);
NodeDto parent = GetParentNodeDto(entity.ParentId);
var level = parent.Level + 1;
// get sort order
var sortOrder = GetNewChildSortOrder(entity.ParentId, 0);
// persist the node dto
var nodeDto = dto.ContentDto.NodeDto;
NodeDto nodeDto = memberDto.ContentDto.NodeDto;
nodeDto.Path = parent.Path;
nodeDto.Level = Convert.ToInt16(level);
nodeDto.SortOrder = sortOrder;
@@ -304,36 +345,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
entity.Level = level;
// persist the content dto
var contentDto = dto.ContentDto;
var contentDto = memberDto.ContentDto;
contentDto.NodeId = nodeDto.NodeId;
Database.Insert(contentDto);
// persist the content version dto
// assumes a new version id and version date (modified date) has been set
var contentVersionDto = dto.ContentVersionDto;
var contentVersionDto = memberDto.ContentVersionDto;
contentVersionDto.NodeId = nodeDto.NodeId;
contentVersionDto.Current = true;
Database.Insert(contentVersionDto);
entity.VersionId = contentVersionDto.Id;
// persist the member dto
dto.NodeId = nodeDto.NodeId;
memberDto.NodeId = nodeDto.NodeId;
// if the password is empty, generate one with the special prefix
// this will hash the guid with a salt so should be nicely random
if (entity.RawPasswordValue.IsNullOrWhiteSpace())
{
dto.Password = Cms.Core.Constants.Security.EmptyPasswordPrefix + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N"));
entity.RawPasswordValue = dto.Password;
memberDto.Password = Cms.Core.Constants.Security.EmptyPasswordPrefix + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N"));
entity.RawPasswordValue = memberDto.Password;
}
Database.Insert(dto);
Database.Insert(memberDto);
// persist the property data
InsertPropertyValues(entity, 0, out _, out _);
SetEntityTags(entity, _tagRepository, _serializer);
SetEntityTags(entity, _tagRepository, _jsonSerializer);
PersistRelations(entity);
@@ -368,17 +409,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
// create the dto
MemberDto dto = ContentBaseFactory.BuildDto(entity);
MemberDto memberDto = ContentBaseFactory.BuildDto(entity);
// update the node dto
NodeDto nodeDto = dto.ContentDto.NodeDto;
NodeDto nodeDto = memberDto.ContentDto.NodeDto;
Database.Update(nodeDto);
// update the content dto
Database.Update(dto.ContentDto);
Database.Update(memberDto.ContentDto);
// update the content version dto
Database.Update(dto.ContentVersionDto);
Database.Update(memberDto.ContentVersionDto);
// update the member dto
// but only the changed columns, 'cos we cannot update password if empty
@@ -399,6 +440,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
changedCols.Add("LoginName");
}
// this can occur from an upgrade
if (memberDto.PasswordConfig.IsNullOrWhiteSpace())
{
memberDto.PasswordConfig = DefaultPasswordConfigJson;
changedCols.Add("passwordConfig");
}
// do NOT update the password if it has not changed or if it is null or empty
if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue))
{
@@ -407,33 +455,37 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
// If the security stamp hasn't already updated we need to force it
if (entity.IsPropertyDirty("SecurityStamp") == false)
{
dto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
changedCols.Add("securityStampToken");
}
// check if we have a user config else use the default
memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson;
changedCols.Add("passwordConfig");
}
// If userlogin or the email has changed then need to reset security stamp
if (changedCols.Contains("Email") || changedCols.Contains("LoginName"))
{
dto.EmailConfirmedDate = null;
memberDto.EmailConfirmedDate = null;
changedCols.Add("emailConfirmedDate");
// If the security stamp hasn't already updated we need to force it
if (entity.IsPropertyDirty("SecurityStamp") == false)
{
dto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
changedCols.Add("securityStampToken");
}
}
if (changedCols.Count > 0)
{
Database.Update(dto, changedCols);
Database.Update(memberDto, changedCols);
}
ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _);
SetEntityTags(entity, _tagRepository, _serializer);
SetEntityTags(entity, _tagRepository, _jsonSerializer);
PersistRelations(entity);

View File

@@ -73,7 +73,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
return _passwordConfigJson;
}
var passwordConfig = new UserPasswordSettings
var passwordConfig = new PersistedPasswordSettings
{
HashAlgorithm = _passwordConfiguration.HashAlgorithmType
};
@@ -462,7 +462,7 @@ ORDER BY colName";
entity.SecurityStamp = Guid.NewGuid().ToString();
}
var userDto = UserFactory.BuildDto(entity);
UserDto userDto = UserFactory.BuildDto(entity);
// check if we have a user config else use the default
userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson;

View File

@@ -1,93 +1,21 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// A password hasher for back office users
/// </summary>
/// <remarks>
/// This allows us to verify passwords in old formats and roll forward to the latest format
/// </remarks>
public class BackOfficePasswordHasher : PasswordHasher<BackOfficeIdentityUser>
public class BackOfficePasswordHasher : UmbracoPasswordHasher<BackOfficeIdentityUser>
{
private readonly LegacyPasswordSecurity _legacyPasswordSecurity;
private readonly IJsonSerializer _jsonSerializer;
private readonly PasswordHasher<BackOfficeIdentityUser> _aspnetV2PasswordHasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
public BackOfficePasswordHasher(LegacyPasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
: base(passwordSecurity, jsonSerializer)
{
_legacyPasswordSecurity = passwordSecurity;
_jsonSerializer = jsonSerializer;
}
public override string HashPassword(BackOfficeIdentityUser user, string password)
{
// Always use the latest/current hash algorithm when hashing new passwords for storage.
// NOTE: This is only overridden to show that we can since we may need to adjust this in the future
// if new/different formats are required.
return base.HashPassword(user, password);
}
/// <summary>
/// Verifies a user's hashed password
/// </summary>
/// <param name="user"></param>
/// <param name="hashedPassword"></param>
/// <param name="providedPassword"></param>
/// <returns></returns>
/// <remarks>
/// This will check the user's current hashed password format stored with their user row and use that to verify the hash. This could be any hashes
/// from the very old v4, to the older v6-v8, to the older aspnet identity and finally to the most recent
/// </remarks>
public override PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
{
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
{
var result = _legacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
return result
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Failed;
}
// We will explicitly detect names here
// The default is PBKDF2.ASPNETCORE.V3:
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
// The PBKDF2.ASPNETCORE.V2 settings are:
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
switch (deserialized.HashAlgorithm)
{
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
if (legacyResult == PasswordVerificationResult.Success)
return PasswordVerificationResult.SuccessRehashNeeded;
return legacyResult;
}
}
// else go the default (v3)
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
private class V2PasswordHasherOptions : IOptions<PasswordHasherOptions>
{
public PasswordHasherOptions Value => new PasswordHasherOptions
{
CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
};
}
}
}

View File

@@ -1,7 +1,10 @@
using System;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security
{
@@ -11,11 +14,12 @@ namespace Umbraco.Cms.Core.Security
/// <remarks>
/// This will check for the ASP.NET Identity password hash flag before falling back to the legacy password hashing format ("HMACSHA256")
/// </remarks>
public class MemberPasswordHasher : PasswordHasher<MemberIdentityUser>
public class MemberPasswordHasher : UmbracoPasswordHasher<MemberIdentityUser>
{
private readonly LegacyPasswordSecurity _legacyPasswordHasher;
public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher) => _legacyPasswordHasher = legacyPasswordHasher ?? throw new ArgumentNullException(nameof(legacyPasswordHasher));
public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher, IJsonSerializer jsonSerializer)
: base(legacyPasswordHasher, jsonSerializer)
{
}
/// <summary>
/// Verifies a user's hashed password
@@ -27,6 +31,20 @@ namespace Umbraco.Cms.Core.Security
/// <exception cref="InvalidOperationException">Thrown when the correct hashing algorith cannot be determined</exception>
public override PasswordVerificationResult VerifyHashedPassword(MemberIdentityUser user, string hashedPassword, string providedPassword)
{
if (user is null)
{
throw new ArgumentNullException(nameof(user));
}
// if there's password config use the base implementation
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
// Else we need to detect what the password is. This will be the case
// for upgrades since no password config will exist.
byte[] decodedHashedPassword = null;
bool isAspNetIdentityHash = false;
@@ -51,7 +69,7 @@ namespace Umbraco.Cms.Core.Security
throw new InvalidOperationException("unable to determine member password hashing algorith");
}
var isValid = _legacyPasswordHasher.VerifyPassword(
var isValid = LegacyPasswordSecurity.VerifyPassword(
Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName,
providedPassword,
hashedPassword);

View File

@@ -224,12 +224,17 @@ namespace Umbraco.Cms.Core.Security
/// <returns></returns>
private UmbracoIdentityRole MapFromMemberGroup(IMemberGroup memberGroup)
{
// NOTE: there is a ConcurrencyStamp property but we don't use it. The purpose
// of this value is to try to prevent concurrent writes in the DB but this is
// an implementation detail at the data source level that has leaked into the
// model. A good writeup of that is here:
// https://stackoverflow.com/a/37362173
// For our purposes currently we won't worry about this.
var result = new UmbracoIdentityRole
{
Id = memberGroup.Id.ToString(),
Name = memberGroup.Name
// TODO: Implement this functionality, requires DB and logic updates
//ConcurrencyStamp
};
return result;
}
@@ -247,8 +252,6 @@ namespace Umbraco.Cms.Core.Security
if (role.IsPropertyDirty(nameof(UmbracoIdentityRole.Name))
&& !string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name)
{
// TODO: Need to support ConcurrencyStamp and logic
memberGroup.Name = role.Name;
anythingChanged = true;
}

View File

@@ -611,6 +611,10 @@ namespace Umbraco.Cms.Core.Security
public IPublishedContent GetPublishedMember(MemberIdentityUser user)
{
if (user == null)
{
return null;
}
IMember member = _memberService.GetByKey(user.Key);
if (member == null)
{

View File

@@ -57,7 +57,12 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public bool HasIdentity { get; protected set; }
// TODO: We should support this and it's logic
// NOTE: The purpose
// of this value is to try to prevent concurrent writes in the DB but this is
// an implementation detail at the data source level that has leaked into the
// model. A good writeup of that is here:
// https://stackoverflow.com/a/37362173
// For our purposes currently we won't worry about this.
public override string ConcurrencyStamp { get => base.ConcurrencyStamp; set => base.ConcurrencyStamp = value; }
/// <summary>

View File

@@ -69,6 +69,14 @@ namespace Umbraco.Cms.Core.Security
}
}
// NOTE: The purpose
// of this value is to try to prevent concurrent writes in the DB but this is
// an implementation detail at the data source level that has leaked into the
// model. A good writeup of that is here:
// https://stackoverflow.com/a/37362173
// For our purposes currently we won't worry about this.
public override string ConcurrencyStamp { get => base.ConcurrencyStamp; set => base.ConcurrencyStamp = value; }
/// <summary>
/// Gets or sets last login date
/// </summary>

View File

@@ -0,0 +1,92 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security
{
public class UmbracoPasswordHasher<TUser> : PasswordHasher<TUser>
where TUser: UmbracoIdentityUser
{
private readonly IJsonSerializer _jsonSerializer;
private readonly PasswordHasher<TUser> _aspnetV2PasswordHasher = new PasswordHasher<TUser>(new V2PasswordHasherOptions());
public UmbracoPasswordHasher(LegacyPasswordSecurity legacyPasswordSecurity, IJsonSerializer jsonSerializer)
{
LegacyPasswordSecurity = legacyPasswordSecurity ?? throw new System.ArgumentNullException(nameof(legacyPasswordSecurity));
_jsonSerializer = jsonSerializer ?? throw new System.ArgumentNullException(nameof(jsonSerializer));
}
public LegacyPasswordSecurity LegacyPasswordSecurity { get; }
public override string HashPassword(TUser user, string password)
{
// Always use the latest/current hash algorithm when hashing new passwords for storage.
// NOTE: This is only overridden to show that we can since we may need to adjust this in the future
// if new/different formats are required.
return base.HashPassword(user, password);
}
/// <summary>
/// Verifies a user's hashed password
/// </summary>
/// <param name="user"></param>
/// <param name="hashedPassword"></param>
/// <param name="providedPassword"></param>
/// <returns></returns>
/// <remarks>
/// This will check the user's current hashed password format stored with their user row and use that to verify the hash. This could be any hashes
/// from the very old v4, to the older v6-v8, to the older aspnet identity and finally to the most recent
/// </remarks>
public override PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
{
if (user is null)
{
throw new System.ArgumentNullException(nameof(user));
}
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<PersistedPasswordSettings>(user.PasswordConfig);
if (LegacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
{
var result = LegacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
return result
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Failed;
}
// We will explicitly detect names here
// The default is PBKDF2.ASPNETCORE.V3:
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
// The PBKDF2.ASPNETCORE.V2 settings are:
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
switch (deserialized.HashAlgorithm)
{
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
if (legacyResult == PasswordVerificationResult.Success)
return PasswordVerificationResult.SuccessRehashNeeded;
return legacyResult;
}
}
// else go the default (v3)
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
private class V2PasswordHasherOptions : IOptions<PasswordHasherOptions>
{
public PasswordHasherOptions Value => new PasswordHasherOptions
{
CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
};
}
}
}

View File

@@ -116,6 +116,7 @@ namespace Umbraco.Cms.Tests.Common.Builders
DateTime lastLockoutDate = _lastLockoutDate ?? DateTime.Now;
DateTime lastLoginDate = _lastLoginDate ?? DateTime.Now;
DateTime lastPasswordChangeDate = _lastPasswordChangeDate ?? DateTime.Now;
var passwordConfig = _passwordConfig ?? "{\"hashAlgorithm\":\"PBKDF2.ASPNETCORE.V3\"}";
if (_memberTypeBuilder is null && _memberType is null)
{
@@ -135,6 +136,7 @@ namespace Umbraco.Cms.Tests.Common.Builders
Path = path,
SortOrder = sortOrder,
Trashed = trashed,
PasswordConfiguration = passwordConfig
};
if (_propertyIdsIncrementingFrom.HasValue)

View File

@@ -18,7 +18,6 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.DependencyInjection;
@@ -181,8 +180,11 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest
public override void Configure(IApplicationBuilder app)
{
app.UseUmbraco()
.WithBackOffice()
.WithWebsite()
.WithMiddleware(u =>
{
u.WithBackOffice();
u.WithWebsite();
})
.WithEndpoints(u =>
{
u.UseBackOfficeEndpoints();

View File

@@ -6,11 +6,14 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NPoco;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -54,7 +57,23 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
IRelationRepository relationRepository = GetRequiredService<IRelationRepository>();
var propertyEditors = new Lazy<PropertyEditorCollection>(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>())));
var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>());
return new MemberRepository(accessor, AppCaches.Disabled, LoggerFactory.CreateLogger<MemberRepository>(), MemberTypeRepository, MemberGroupRepository, tagRepo, Mock.Of<ILanguageRepository>(), relationRepository, relationTypeRepository, PasswordHasher, propertyEditors, dataValueReferences, DataTypeService, JsonSerializer, Mock.Of<IEventAggregator>());
return new MemberRepository(
accessor,
AppCaches.Disabled,
LoggerFactory.CreateLogger<MemberRepository>(),
MemberTypeRepository,
MemberGroupRepository,
tagRepo,
Mock.Of<ILanguageRepository>(),
relationRepository,
relationTypeRepository,
PasswordHasher,
propertyEditors,
dataValueReferences,
DataTypeService,
JsonSerializer,
Mock.Of<IEventAggregator>(),
Options.Create(new MemberPasswordConfigurationSettings()));
}
[Test]

View File

@@ -3,13 +3,14 @@ using Microsoft.AspNetCore.Identity;
using NUnit.Framework;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberPasswordHasherTests
{
private MemberPasswordHasher CreateSut() => new MemberPasswordHasher(new LegacyPasswordSecurity());
private MemberPasswordHasher CreateSut() => new MemberPasswordHasher(new LegacyPasswordSecurity(), new JsonNetSerializer());
[Test]
public void VerifyHashedPassword_GivenAnAspNetIdentity2PasswordHash_ThenExpectSuccessRehashNeeded()
@@ -18,7 +19,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
const string hash = "AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}
@@ -30,7 +31,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
const string hash = "AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
Assert.AreEqual(result, PasswordVerificationResult.Success);
}
@@ -42,7 +43,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
const string hash = "yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, password);
Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}
@@ -54,7 +55,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
var hash = Convert.ToBase64String(hashBytes);
var sut = CreateSut();
Assert.Throws<InvalidOperationException>(() => sut.VerifyHashedPassword(null, hash, "password"));
Assert.Throws<InvalidOperationException>(() => sut.VerifyHashedPassword(new MemberIdentityUser(), hash, "password"));
}
[TestCase("AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==")]
@@ -65,7 +66,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
const string invalidPassword = "nope";
var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, invalidPassword);
var result = sut.VerifyHashedPassword(new MemberIdentityUser(), hash, invalidPassword);
Assert.AreEqual(result, PasswordVerificationResult.Failed);
}

View File

@@ -21,7 +21,7 @@ namespace Umbraco.Extensions
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IUmbracoApplicationBuilder WithBackOffice(this IUmbracoApplicationBuilder builder)
public static IUmbracoMiddlewareBuilder WithBackOffice(this IUmbracoMiddlewareBuilder builder)
{
KeepAliveSettings keepAliveSettings = builder.ApplicationServices.GetRequiredService<IOptions<KeepAliveSettings>>().Value;
IHostingEnvironment hostingEnvironment = builder.ApplicationServices.GetRequiredService<IHostingEnvironment>();

View File

@@ -1,14 +1,17 @@
using System;
using System;
using Microsoft.AspNetCore.Builder;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.ApplicationBuilder
{
public interface IUmbracoApplicationBuilder
public interface IUmbracoApplicationBuilder : IUmbracoMiddlewareBuilder
{
IRuntimeState RuntimeState { get; }
IServiceProvider ApplicationServices { get; }
IApplicationBuilder AppBuilder { get; }
/// <summary>
/// Called to include umbraco middleware
/// </summary>
/// <param name="configureUmbraco"></param>
/// <returns></returns>
IUmbracoApplicationBuilder WithMiddleware(Action<IUmbracoMiddlewareBuilder> configureUmbraco);
/// <summary>
/// Final call during app building to configure endpoints

View File

@@ -1,7 +1,4 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.ApplicationBuilder
{
@@ -9,11 +6,8 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder
/// <summary>
/// A builder to allow encapsulating the enabled routing features in Umbraco
/// </summary>
public interface IUmbracoEndpointBuilder
{
IRuntimeState RuntimeState { get; }
IServiceProvider ApplicationServices { get; }
IEndpointRouteBuilder EndpointRouteBuilder { get; }
IApplicationBuilder AppBuilder { get; }
public interface IUmbracoEndpointBuilder : IUmbracoMiddlewareBuilder
{
IEndpointRouteBuilder EndpointRouteBuilder { get; }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using Microsoft.AspNetCore.Builder;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.ApplicationBuilder
{
public interface IUmbracoMiddlewareBuilder
{
IRuntimeState RuntimeState { get; }
IServiceProvider ApplicationServices { get; }
IApplicationBuilder AppBuilder { get; }
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.ApplicationBuilder
@@ -21,13 +22,44 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder
public IRuntimeState RuntimeState { get; }
public IApplicationBuilder AppBuilder { get; }
public IUmbracoApplicationBuilder WithMiddleware(Action<IUmbracoMiddlewareBuilder> configureUmbraco)
{
IOptions<UmbracoPipelineOptions> startupOptions = ApplicationServices.GetRequiredService<IOptions<UmbracoPipelineOptions>>();
RunPostPipeline(startupOptions.Value);
configureUmbraco(this);
return this;
}
public void WithEndpoints(Action<IUmbracoEndpointBuilder> configureUmbraco)
=> AppBuilder.UseEndpoints(endpoints =>
{
var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance<UmbracoEndpointBuilder>(
ApplicationServices,
new object[] { AppBuilder, endpoints });
configureUmbraco(umbAppBuilder);
});
{
IOptions<UmbracoPipelineOptions> startupOptions = ApplicationServices.GetRequiredService<IOptions<UmbracoPipelineOptions>>();
RunPreEndpointsPipeline(startupOptions.Value);
AppBuilder.UseEndpoints(endpoints =>
{
var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance<UmbracoEndpointBuilder>(
ApplicationServices,
new object[] { AppBuilder, endpoints });
configureUmbraco(umbAppBuilder);
});
}
private void RunPostPipeline(UmbracoPipelineOptions startupOptions)
{
foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters)
{
filter.OnPostPipeline(AppBuilder);
}
}
private void RunPreEndpointsPipeline(UmbracoPipelineOptions startupOptions)
{
foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters)
{
filter.OnEndpoints(AppBuilder);
}
}
}
}

View File

@@ -1,5 +1,3 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
@@ -50,15 +48,7 @@ namespace Umbraco.Extensions
services.AddScoped<IPasswordHasher<MemberIdentityUser>, MemberPasswordHasher>();
services.ConfigureOptions<ConfigureSecurityStampOptions>();
services.ConfigureApplicationCookie(x =>
{
// TODO: We may want/need to configure these further
x.LoginPath = null;
x.AccessDeniedPath = null;
x.LogoutPath = null;
});
services.ConfigureOptions<ConfigureMemberCookieOptions>();
return builder;
}

View File

@@ -72,9 +72,6 @@ namespace Umbraco.Extensions
// DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline,
// endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder
app.RunPostPipeline(startupOptions.Value);
app.RunPreEndpointsPipeline(startupOptions.Value);
return ActivatorUtilities.CreateInstance<UmbracoApplicationBuilder>(
app.ApplicationServices,
new object[] { app });
@@ -88,22 +85,6 @@ namespace Umbraco.Extensions
}
}
private static void RunPostPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions)
{
foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters)
{
filter.OnPostPipeline(app);
}
}
private static void RunPreEndpointsPipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions)
{
foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters)
{
filter.OnEndpoints(app);
}
}
/// <summary>
/// Returns true if Umbraco <see cref="IRuntimeState"/> is greater than <see cref="RuntimeLevel.BootFailed"/>
/// </summary>

View File

@@ -7,14 +7,12 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security
{
// TODO: This is only for the back office, does it need to be in common?
public class BackOfficeSecurity : IBackOfficeSecurity
{
private readonly IUserService _userService;
private readonly IHttpContextAccessor _httpContextAccessor;
private object _currentUserLock = new object();
private readonly object _currentUserLock = new object();
private IUser _currentUser;
public BackOfficeSecurity(

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.Security
{
public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
{
private readonly IRuntimeState _runtimeState;
private readonly UmbracoRequestPaths _umbracoRequestPaths;
public ConfigureMemberCookieOptions(IRuntimeState runtimeState, UmbracoRequestPaths umbracoRequestPaths)
{
_runtimeState = runtimeState;
_umbracoRequestPaths = umbracoRequestPaths;
}
public void Configure(string name, CookieAuthenticationOptions options)
{
if (name == IdentityConstants.ApplicationScheme || name == IdentityConstants.ExternalScheme)
{
Configure(options);
}
}
public void Configure(CookieAuthenticationOptions options)
{
// TODO: We may want/need to configure these further
options.LoginPath = null;
options.AccessDeniedPath = null;
options.LogoutPath = null;
options.CookieManager = new MemberCookieManager(_runtimeState, _umbracoRequestPaths);
}
}
}

View File

@@ -6,14 +6,13 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security
{
public sealed class ConfigureMemberIdentityOptions : IConfigureOptions<IdentityOptions>
{
private readonly MemberPasswordConfigurationSettings _memberPasswordConfiguration;
public ConfigureMemberIdentityOptions(IOptions<MemberPasswordConfigurationSettings> memberPasswordConfiguration)
{
_memberPasswordConfiguration = memberPasswordConfiguration.Value;
}
=> _memberPasswordConfiguration = memberPasswordConfiguration.Value;
public void Configure(IdentityOptions options)
{

View File

@@ -0,0 +1,70 @@
using System;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Web.Common.Security
{
/// <summary>
/// A custom cookie manager for members to ensure that cookie auth does not occur for any back office requests
/// </summary>
public class MemberCookieManager : ChunkingCookieManager, ICookieManager
{
private readonly IRuntimeState _runtime;
private readonly UmbracoRequestPaths _umbracoRequestPaths;
public MemberCookieManager(IRuntimeState runtime, UmbracoRequestPaths umbracoRequestPaths)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_umbracoRequestPaths = umbracoRequestPaths ?? throw new ArgumentNullException(nameof(umbracoRequestPaths));
}
/// <summary>
/// Determines if we should authenticate the request
/// </summary>
/// <returns>true if the request should be authenticated</returns>
/// <remarks>
/// We auth the request when it is not a back office request and when the runtime level is Run
/// </remarks>
public bool ShouldAuthenticateRequest(string absPath)
{
// Do not authenticate the request if we are not running.
// Else this can cause problems especially if the members DB table needs upgrades
// because when authing, the member db table will be read and we'll get exceptions.
if (_runtime.Level != RuntimeLevel.Run)
{
return false;
}
if (// check back office
_umbracoRequestPaths.IsBackOfficeRequest(absPath)
// check installer
|| _umbracoRequestPaths.IsInstallerRequest(absPath))
{
return false;
}
return true;
}
/// <summary>
/// Explicitly implement this so that we filter the request
/// </summary>
/// <inheritdoc/>
string ICookieManager.GetRequestCookie(HttpContext context, string key)
{
var absPath = context.Request.Path;
return ShouldAuthenticateRequest(absPath) == false
// Don't auth request, don't return a cookie
? null
// Return the default implementation
: GetRequestCookie(context, key);
}
}
}

View File

@@ -108,9 +108,7 @@ namespace Umbraco.Cms.Web.Common
/// <param name="alias">The alias.</param>
/// <returns></returns>
public async Task<IHtmlEncodedString> RenderMacroAsync(string alias)
{
return await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, null);
}
=> await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, null);
/// <summary>
/// Renders the macro with the specified alias, passing in the specified parameters.
@@ -119,9 +117,7 @@ namespace Umbraco.Cms.Web.Common
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
public async Task<IHtmlEncodedString> RenderMacroAsync(string alias, object parameters)
{
return await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters?.ToDictionary<object>());
}
=> await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters?.ToDictionary<object>());
/// <summary>
/// Renders the macro with the specified alias, passing in the specified parameters.
@@ -130,9 +126,7 @@ namespace Umbraco.Cms.Web.Common
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
public async Task<IHtmlEncodedString> RenderMacroAsync(string alias, IDictionary<string, object> parameters)
{
return await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters);
}
=> await _componentRenderer.RenderMacroAsync(AssignedContentItem?.Id ?? 0, alias, parameters);
#endregion

View File

@@ -60,8 +60,11 @@ namespace Umbraco.Cms.Web.UI.NetCore
}
app.UseUmbraco()
.WithBackOffice()
.WithWebsite()
.WithMiddleware(u =>
{
u.WithBackOffice();
u.WithWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();

View File

@@ -17,7 +17,7 @@ namespace Umbraco.Extensions
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IUmbracoApplicationBuilder WithWebsite(this IUmbracoApplicationBuilder builder)
public static IUmbracoMiddlewareBuilder WithWebsite(this IUmbracoMiddlewareBuilder builder)
{
builder.AppBuilder.UseMiddleware<PublicAccessMiddleware>();
return builder;