Make identity rehash persistence best effort
This commit is contained in:
parent
95640cbf7d
commit
94df64b2b9
|
|
@ -2,6 +2,8 @@ using Elsa.Extensions;
|
|||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Elsa.Identity.Services;
|
||||
|
||||
|
|
@ -13,16 +15,25 @@ public class DefaultApplicationCredentialsValidator : IApplicationCredentialsVal
|
|||
private readonly IApplicationProvider _applicationProvider;
|
||||
private readonly IApplicationStore _applicationStore;
|
||||
private readonly ISecretHasher _secretHasher;
|
||||
private readonly ILogger<DefaultApplicationCredentialsValidator> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultApplicationCredentialsValidator"/> class.
|
||||
/// </summary>
|
||||
public DefaultApplicationCredentialsValidator(IApiKeyParser apiKeyParser, IApplicationProvider applicationProvider, IApplicationStore applicationStore, ISecretHasher secretHasher)
|
||||
public DefaultApplicationCredentialsValidator(IApiKeyParser apiKeyParser, IApplicationProvider applicationProvider, IApplicationStore applicationStore, ISecretHasher secretHasher) : this(apiKeyParser, applicationProvider, applicationStore, secretHasher, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultApplicationCredentialsValidator"/> class.
|
||||
/// </summary>
|
||||
public DefaultApplicationCredentialsValidator(IApiKeyParser apiKeyParser, IApplicationProvider applicationProvider, IApplicationStore applicationStore, ISecretHasher secretHasher, ILogger<DefaultApplicationCredentialsValidator>? logger)
|
||||
{
|
||||
_apiKeyParser = apiKeyParser;
|
||||
_applicationProvider = applicationProvider;
|
||||
_applicationStore = applicationStore;
|
||||
_secretHasher = secretHasher;
|
||||
_logger = logger ?? NullLogger<DefaultApplicationCredentialsValidator>.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -47,7 +58,14 @@ public class DefaultApplicationCredentialsValidator : IApplicationCredentialsVal
|
|||
var hashedApiKey = _secretHasher.HashSecret(apiKey);
|
||||
application.HashedApiKey = hashedApiKey.EncodeSecret();
|
||||
application.HashedApiKeySalt = hashedApiKey.EncodeSalt();
|
||||
await _applicationStore.SaveAsync(application, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _applicationStore.SaveAsync(application, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to save upgraded API key hash for application {ApplicationId}.", application.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return application;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Elsa.Identity.Services;
|
||||
|
||||
|
|
@ -12,15 +14,24 @@ public class DefaultUserCredentialsValidator : IUserCredentialsValidator
|
|||
private readonly IUserProvider _userProvider;
|
||||
private readonly IUserStore _userStore;
|
||||
private readonly ISecretHasher _secretHasher;
|
||||
private readonly ILogger<DefaultUserCredentialsValidator> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUserCredentialsValidator"/> class.
|
||||
/// </summary>
|
||||
public DefaultUserCredentialsValidator(IUserProvider userProvider, IUserStore userStore, ISecretHasher secretHasher)
|
||||
public DefaultUserCredentialsValidator(IUserProvider userProvider, IUserStore userStore, ISecretHasher secretHasher) : this(userProvider, userStore, secretHasher, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUserCredentialsValidator"/> class.
|
||||
/// </summary>
|
||||
public DefaultUserCredentialsValidator(IUserProvider userProvider, IUserStore userStore, ISecretHasher secretHasher, ILogger<DefaultUserCredentialsValidator>? logger)
|
||||
{
|
||||
_userProvider = userProvider;
|
||||
_userStore = userStore;
|
||||
_secretHasher = secretHasher;
|
||||
_logger = logger ?? NullLogger<DefaultUserCredentialsValidator>.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -41,7 +52,14 @@ public class DefaultUserCredentialsValidator : IUserCredentialsValidator
|
|||
var hashedPassword = _secretHasher.HashSecret(password);
|
||||
user.HashedPassword = hashedPassword.EncodeSecret();
|
||||
user.HashedPasswordSalt = hashedPassword.EncodeSalt();
|
||||
await _userStore.SaveAsync(user, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _userStore.SaveAsync(user, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to save upgraded password hash for user {UserId}.", user.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Elsa.Common.Services;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
using Elsa.Identity.Providers;
|
||||
|
|
@ -117,6 +118,50 @@ public class DefaultSecretHasherTests
|
|||
Assert.StartsWith("pbkdf2-sha256$", Encoding.UTF8.GetString(Convert.FromBase64String(reloadedApplication.HashedApiKey)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ReturnsUserWhenLegacyPasswordRehashSaveFails()
|
||||
{
|
||||
var legacyHash = CreateLegacyHash("secret");
|
||||
var user = new User
|
||||
{
|
||||
Id = "user-1",
|
||||
Name = "alice",
|
||||
HashedPassword = legacyHash.EncodeSecret(),
|
||||
HashedPasswordSalt = legacyHash.EncodeSalt()
|
||||
};
|
||||
var userStore = new FailingUserStore(user);
|
||||
var validator = new DefaultUserCredentialsValidator(new StoreBasedUserProvider(userStore), userStore, _hasher);
|
||||
|
||||
var validatedUser = await validator.ValidateAsync("alice", "secret");
|
||||
|
||||
Assert.Same(user, validatedUser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ReturnsApplicationWhenLegacyApiKeyRehashSaveFails()
|
||||
{
|
||||
var apiKeyGenerator = new DefaultApiKeyGeneratorAndParser();
|
||||
var apiKey = apiKeyGenerator.Generate("client-1");
|
||||
var legacyHash = CreateLegacyHash(apiKey);
|
||||
var application = new Application
|
||||
{
|
||||
Id = "app-1",
|
||||
ClientId = "client-1",
|
||||
Name = "Client 1",
|
||||
HashedApiKey = legacyHash.EncodeSecret(),
|
||||
HashedApiKeySalt = legacyHash.EncodeSalt(),
|
||||
HashedClientSecret = "",
|
||||
HashedClientSecretSalt = ""
|
||||
};
|
||||
var applicationStore = new FailingApplicationStore(application);
|
||||
var applicationProvider = new StoreBasedApplicationProvider(applicationStore);
|
||||
var validator = new DefaultApplicationCredentialsValidator(apiKeyGenerator, applicationProvider, applicationStore, _hasher);
|
||||
|
||||
var validatedApplication = await validator.ValidateAsync(apiKey);
|
||||
|
||||
Assert.Same(application, validatedApplication);
|
||||
}
|
||||
|
||||
private static HashedSecret CreateLegacyHash(string secret)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(32);
|
||||
|
|
@ -133,4 +178,40 @@ public class DefaultSecretHasherTests
|
|||
var envelope = Encoding.UTF8.GetBytes($"pbkdf2-sha256${iterationCount}${Convert.ToBase64String(hash)}");
|
||||
return HashedSecret.FromBytes(envelope, salt);
|
||||
}
|
||||
|
||||
private sealed class FailingUserStore(User user) : IUserStore
|
||||
{
|
||||
private readonly User _user = user;
|
||||
|
||||
public Task SaveAsync(User user, CancellationToken cancellationToken = default) => throw new InvalidOperationException("Save failed.");
|
||||
|
||||
public Task DeleteAsync(UserFilter filter, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public Task<IEnumerable<User>> FindManyAsync(UserFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var users = filter.Apply(new[] { _user }.AsQueryable()).ToList();
|
||||
return Task.FromResult<IEnumerable<User>>(users);
|
||||
}
|
||||
|
||||
public Task<User?> FindAsync(UserFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = filter.Apply(new[] { _user }.AsQueryable()).FirstOrDefault();
|
||||
return Task.FromResult(user);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailingApplicationStore(Application application) : IApplicationStore
|
||||
{
|
||||
private readonly Application _application = application;
|
||||
|
||||
public Task SaveAsync(Application application, CancellationToken cancellationToken = default) => throw new InvalidOperationException("Save failed.");
|
||||
|
||||
public Task DeleteAsync(ApplicationFilter filter, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
public Task<Application?> FindAsync(ApplicationFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var application = filter.Apply(new[] { _application }.AsQueryable()).FirstOrDefault();
|
||||
return Task.FromResult(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue