Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal sealed class CredentialManagementService : ICredentialManagementService
private readonly ISessionStoreFactory _sessionFactory;
private readonly ICredentialResetNotifier _resetNotifier;
private readonly ICredentialSessionPolicy _credentialSessionPolicy;
private readonly IUAuthAtomicExecutor _atomicExecutor;
private readonly UAuthServerOptions _options;
private readonly IClock _clock;

Expand All @@ -42,6 +43,7 @@ public CredentialManagementService(
ISessionStoreFactory sessionFactory,
ICredentialResetNotifier resetNotifier,
ICredentialSessionPolicy credentialSessionPolicy,
IUAuthAtomicExecutor atomicExecutor,
IOptions<UAuthServerOptions> options,
IClock clock)
{
Expand All @@ -56,6 +58,7 @@ public CredentialManagementService(
_sessionFactory = sessionFactory;
_resetNotifier = resetNotifier;
_credentialSessionPolicy = credentialSessionPolicy;
_atomicExecutor = atomicExecutor;
_options = options.Value;
_clock = clock;
}
Expand Down Expand Up @@ -125,48 +128,49 @@ public async Task<CredentialChangeResult> ChangeSecretAsync(AccessContext contex
{
ct.ThrowIfCancellationRequested();

var cmd = new AccessCommand<CredentialChangeResult>(async innerCt =>
{
var subjectUser = context.GetTargetUserKey();
var now = _clock.UtcNow;
var cmd = new AccessCommand<CredentialChangeResult>(
innerCt => _atomicExecutor.ExecuteAsync(async atomicCt =>
{
var subjectUser = context.GetTargetUserKey();
var now = _clock.UtcNow;

var store = _credentialsFactory.Create(context.ResourceTenant);
var credentials = await store.GetByUserAsync(subjectUser, innerCt);
var pwd = credentials.OfType<PasswordCredential>().Where(c => c.Security.IsUsable(now)).FirstOrDefault();
var store = _credentialsFactory.Create(context.ResourceTenant);
var credentials = await store.GetByUserAsync(subjectUser, atomicCt);
var pwd = credentials.OfType<PasswordCredential>().Where(c => c.Security.IsUsable(now)).FirstOrDefault();

if (pwd is null)
throw new UAuthNotFoundException("credential_not_found");
if (pwd is null)
throw new UAuthNotFoundException("credential_not_found");

if (pwd.UserKey != subjectUser)
throw new UAuthNotFoundException("credential_not_found");
if (pwd.UserKey != subjectUser)
throw new UAuthNotFoundException("credential_not_found");

if (string.IsNullOrWhiteSpace(request.NewSecret))
throw new UAuthValidationException("new_secret_required");
if (string.IsNullOrWhiteSpace(request.NewSecret))
throw new UAuthValidationException("new_secret_required");

if (context.IsSelfAction)
{
if (string.IsNullOrWhiteSpace(request.CurrentSecret))
throw new UAuthValidationException("current_secret_required");
if (context.IsSelfAction)
{
if (string.IsNullOrWhiteSpace(request.CurrentSecret))
throw new UAuthValidationException("current_secret_required");

if (!_hasher.Verify(pwd.SecretHash, request.CurrentSecret))
throw new UAuthConflictException("invalid_credentials");
}
if (!_hasher.Verify(pwd.SecretHash, request.CurrentSecret))
throw new UAuthConflictException("invalid_credentials");
}

if (_hasher.Verify(pwd.SecretHash, request.NewSecret))
throw new UAuthValidationException("credential_secret_same");
if (_hasher.Verify(pwd.SecretHash, request.NewSecret))
throw new UAuthValidationException("credential_secret_same");

var oldVersion = pwd.Version;
var newHash = _hasher.Hash(request.NewSecret);
var updated = pwd.ChangeSecret(newHash, now);
await store.SaveAsync(updated, oldVersion, innerCt);
var oldVersion = pwd.Version;
var newHash = _hasher.Hash(request.NewSecret);
var updated = pwd.ChangeSecret(newHash, now);
await store.SaveAsync(updated, oldVersion, atomicCt);

await ApplyCredentialSessionPolicyAsync(context.ResourceTenant, subjectUser, context.IsSelfAction
? CredentialChangeReason.SelfChange
: CredentialChangeReason.AdminChange,
context.ActorChainId, now, innerCt);
await ApplyCredentialSessionPolicyAsync(context.ResourceTenant, subjectUser, context.IsSelfAction
? CredentialChangeReason.SelfChange
: CredentialChangeReason.AdminChange,
context.ActorChainId, now, atomicCt);

return CredentialChangeResult.Success(pwd.Type);
});
return CredentialChangeResult.Success(pwd.Type);
}, innerCt));

return await _accessOrchestrator.ExecuteAsync(context, cmd, ct);
}
Expand Down Expand Up @@ -372,18 +376,24 @@ await _authenticationSecurityManager.UpdateAsync(
return CredentialActionResult.Success();
}

var version3 = state.SecurityVersion;
state = state.ConsumeReset(now);
await _authenticationSecurityManager.UpdateAsync(state, version3, innerCt);
return await _atomicExecutor.ExecuteAsync(async atomicCt =>
{
var version = state.SecurityVersion;
var consumed = state.ConsumeReset(now);

var oldVersion = pwd.Version;
var newHash = _hasher.Hash(request.NewSecret);
var updated = pwd.ChangeSecret(newHash, now);
await _authenticationSecurityManager.UpdateAsync(consumed, version, atomicCt);

await store.SaveAsync(updated, oldVersion, innerCt);
await ApplyCredentialSessionPolicyAsync(context.ResourceTenant ,userKey, CredentialChangeReason.Recovery, actorChainId: null, now, innerCt);
var oldVersion = pwd.Version;
var newHash = _hasher.Hash(request.NewSecret);
var updated = pwd.ChangeSecret(newHash, now);

return CredentialActionResult.Success();
await store.SaveAsync(updated, oldVersion, atomicCt);

await ApplyCredentialSessionPolicyAsync(context.ResourceTenant, userKey, CredentialChangeReason.Recovery, actorChainId: null,
now, atomicCt);

return CredentialActionResult.Success();
}, innerCt);
});

return await _accessOrchestrator.ExecuteAsync(context, cmd, ct);
Expand Down Expand Up @@ -456,7 +466,7 @@ async Task<CredentialActionResult> IUserCredentialsInternalService.DeleteInterna
// HELPERS
// ----------------------------------------
private async Task ApplyCredentialSessionPolicyAsync(TenantKey tenant, UserKey subjectUser, CredentialChangeReason reason,
SessionChainId? actorChainId, DateTimeOffset now, CancellationToken ct)
SessionChainId? actorChainId, DateTimeOffset now, CancellationToken ct)
{
var behavior = _credentialSessionPolicy.Resolve(reason);

Expand All @@ -465,21 +475,22 @@ private async Task ApplyCredentialSessionPolicyAsync(TenantKey tenant, UserKey s

var sessionStore = _sessionFactory.Create(tenant);

switch (behavior)
await sessionStore.ExecuteAsync(async innerCt =>
{
case CredentialSessionBehavior.PreserveCurrent
when reason == CredentialChangeReason.SelfChange && actorChainId is SessionChainId chainId:
await sessionStore.RevokeOtherChainsAsync(subjectUser, chainId, now, ct);
break;

case CredentialSessionBehavior.PreserveCurrent:
case CredentialSessionBehavior.RevokeAll:
switch (behavior)
{
case CredentialSessionBehavior.PreserveCurrent when reason == CredentialChangeReason.SelfChange && actorChainId is SessionChainId chainId:
await sessionStore.RevokeOtherChainsAsync(subjectUser, chainId, now, innerCt);
break;

await sessionStore.RevokeAllChainsAsync(subjectUser, now, ct);
break;
case CredentialSessionBehavior.PreserveCurrent:
case CredentialSessionBehavior.RevokeAll:
await sessionStore.RevokeAllChainsAsync(subjectUser, now, innerCt);
break;

default:
throw new InvalidOperationException($"Unsupported credential session behavior: {behavior}");
}
default:
throw new InvalidOperationException($"Unsupported credential session behavior: {behavior}");
}
}, ct);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,33 @@ public Task ExecuteAsync(Func<CancellationToken, Task> operation, CancellationTo
public async Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> operation, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(operation);
ct.ThrowIfCancellationRequested();

// Participate in an already active transaction.
if (_db.Database.CurrentTransaction is not null)
{
return await operation(ct);
}

await using var transaction = await _db.Database.BeginTransactionAsync(ct);
var strategy = _db.Database.CreateExecutionStrategy();

try
return await strategy.ExecuteAsync(async () =>
{
var result = await operation(ct);
await using var transaction =
await _db.Database.BeginTransactionAsync(ct);

await transaction.CommitAsync(ct);
try
{
var result = await operation(ct);

return result;
}
catch
{
await transaction.RollbackAsync(CancellationToken.None);
await _db.SaveChangesAsync(ct);

throw;
}
await transaction.CommitAsync(ct);

return result;
}
catch
{
await transaction.RollbackAsync(CancellationToken.None);
throw;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,71 +23,75 @@ public EfCoreSessionStore(TDbContext db, TenantExecutionContext tenant)
private DbSet<SessionChainProjection> DbSetChain => _db.Set<SessionChainProjection>();
private DbSet<SessionRootProjection> DbSetRoot => _db.Set<SessionRootProjection>();

public async Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
public Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
{
var strategy = _db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct);
ArgumentNullException.ThrowIfNull(action);

try
{
_inExecution = true;
await action(ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
catch (DbUpdateConcurrencyException)
return ExecuteAsync<object?>(
async innerCt =>
{
await tx.RollbackAsync(ct);
throw new UAuthConcurrencyException("concurrency_conflict");
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
finally
{
_inExecution = false;
}
});
await action(innerCt);
return null;
},
ct);
}

public async Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(action);
ct.ThrowIfCancellationRequested();

// Participate in the existing transaction.
if (_db.Database.CurrentTransaction is not null)
{
return await ExecuteCoreAsync(action, ct);
}

var strategy = _db.Database.CreateExecutionStrategy();

return await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct);
await using var transaction = await _db.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, ct);

try
{
_inExecution = true;
var result = await action(ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
var result = await ExecuteCoreAsync(action, ct);
await transaction.CommitAsync(ct);
return result;
}
catch (DbUpdateConcurrencyException)
{
await tx.RollbackAsync(ct);
throw new UAuthConcurrencyException("concurrency_conflict");
}
catch
{
await tx.RollbackAsync(ct);
await transaction.RollbackAsync(CancellationToken.None);
throw;
}
finally
{
_inExecution = false;
}
});
}

private async Task<TResult> ExecuteCoreAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct)
{
if (_inExecution)
throw new InvalidOperationException("A session store execution is already active.");

_inExecution = true;

try
{
var result = await action(ct);

await _db.SaveChangesAsync(ct);

return result;
}
catch (DbUpdateConcurrencyException)
{
throw new UAuthConcurrencyException("concurrency_conflict");
}
finally
{
_inExecution = false;
}
}

public async Task<UAuthSession?> GetSessionAsync(AuthSessionId sessionId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
Expand Down
Loading
Loading