166 lines
8.2 KiB
C#
166 lines
8.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Application.Auth;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Operations;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.Auth;
|
|
|
|
internal sealed class OwnerActivationService(
|
|
ITenantExecutionScope tenantExecutionScope,
|
|
ITenantRuntimeCacheInvalidator runtimeCacheInvalidator) : IOwnerActivationService
|
|
{
|
|
public async Task CompleteAsync(
|
|
CompleteOwnerActivationRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await CompleteCoreAsync(request, null, null, null, null, false, cancellationToken);
|
|
}
|
|
|
|
public async Task<AuthenticationResult> CompleteAndAuthenticateAsync(
|
|
CompleteOwnerActivationRequest request,
|
|
Guid expectedTenantId,
|
|
string expectedHost,
|
|
string? ipAddress,
|
|
string? userAgent,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var result = await CompleteCoreAsync(
|
|
request, expectedTenantId, expectedHost, ipAddress, userAgent, true, cancellationToken)
|
|
?? throw Error("Owner activation session could not be established.", "owner_activation_failed");
|
|
await runtimeCacheInvalidator.InvalidateAsync(expectedTenantId, cancellationToken);
|
|
return result;
|
|
}
|
|
|
|
private Task<AuthenticationResult?> CompleteCoreAsync(
|
|
CompleteOwnerActivationRequest request,
|
|
Guid? expectedTenantId,
|
|
string? expectedHost,
|
|
string? ipAddress,
|
|
string? userAgent,
|
|
bool authenticate,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return tenantExecutionScope.ExecuteAsync(
|
|
new SystemScopeRequest(
|
|
null,
|
|
SystemScopeCallerType.Anonymous,
|
|
nameof(OwnerActivationService),
|
|
"Complete tenant owner activation",
|
|
request.ActivationId.ToString("N"),
|
|
true),
|
|
async (services, token) =>
|
|
{
|
|
var dbContext = services.GetRequiredService<IOwnerActivationPersistence>();
|
|
var grant = await dbContext.TenantOwnerActivationGrants
|
|
.SingleOrDefaultAsync(value => value.Id == request.ActivationId, token)
|
|
?? throw Error("Owner activation was not found.", "owner_activation_invalid");
|
|
var now = DateTimeOffset.UtcNow;
|
|
var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(request.Token)))
|
|
.ToLowerInvariant();
|
|
if (grant.ConsumedAt.HasValue)
|
|
throw Error("Owner activation was already consumed.", "owner_activation_consumed");
|
|
|
|
if (grant.RevokedAt.HasValue || grant.ExpiresAt <= now || !CryptographicOperations.FixedTimeEquals(
|
|
Convert.FromHexString(grant.TokenHash),
|
|
Convert.FromHexString(tokenHash)))
|
|
throw Error("Owner activation is invalid or expired.", "owner_activation_invalid");
|
|
|
|
if (expectedTenantId.HasValue)
|
|
{
|
|
if (grant.TenantId != expectedTenantId || grant.DomainId is not { } domainId)
|
|
throw Error("Owner activation does not belong to this tenant host.",
|
|
"owner_activation_host_mismatch");
|
|
|
|
var normalizedHost = expectedHost!.Trim().TrimEnd('.').ToLowerInvariant();
|
|
var domainMatches = await dbContext.TenantDomains.AsNoTracking().AnyAsync(value =>
|
|
value.Id == domainId && value.TenantId == grant.TenantId && value.IsPrimary &&
|
|
value.Status == TenantDomainStatus.Active && value.Host == normalizedHost, token);
|
|
if (!domainMatches)
|
|
throw Error("Owner activation does not belong to this tenant host.",
|
|
"owner_activation_host_mismatch");
|
|
}
|
|
|
|
var claimed = await dbContext.TenantOwnerActivationGrants
|
|
.Where(value => value.Id == grant.Id && value.ConsumedAt == null && value.RevokedAt == null &&
|
|
value.ExpiresAt > now && value.TokenHash == tokenHash)
|
|
.ExecuteUpdateAsync(setters => setters
|
|
.SetProperty(value => value.ConsumedAt, now)
|
|
.SetProperty(value => value.UpdatedAt, now), token);
|
|
if (claimed != 1)
|
|
throw Error("Owner activation is invalid or already consumed.", "owner_activation_consumed");
|
|
|
|
var userManager = services.GetRequiredService<UserManager<User>>();
|
|
var user = await userManager.FindByIdAsync(grant.UserId.ToString())
|
|
?? throw Error("Owner account was not found.", "owner_activation_invalid");
|
|
if (await userManager.HasPasswordAsync(user))
|
|
throw Error("Owner account was already activated.", "owner_activation_consumed");
|
|
|
|
var result = await userManager.AddPasswordAsync(user, request.NewPassword);
|
|
if (!result.Succeeded)
|
|
throw Error(
|
|
string.Join("; ", result.Errors.Select(error => error.Description)),
|
|
"owner_activation_password_invalid");
|
|
|
|
user.ForcePasswordChange = false;
|
|
var updateResult = await userManager.UpdateAsync(user);
|
|
if (!updateResult.Succeeded)
|
|
throw Error("Owner account activation could not be completed.", "owner_activation_failed");
|
|
|
|
await userManager.UpdateSecurityStampAsync(user);
|
|
dbContext.AuditLogs.Add(new AuditLog
|
|
{
|
|
TenantId = grant.TenantId,
|
|
ActorUserId = grant.UserId,
|
|
Action = "tenant.owner.activated",
|
|
TargetType = "users",
|
|
TargetId = grant.UserId.ToString()
|
|
});
|
|
|
|
AuthenticationResult? authentication = null;
|
|
if (authenticate)
|
|
{
|
|
var tenant = await dbContext.Tenants.SingleAsync(value => value.Id == grant.TenantId, token);
|
|
var membership = await dbContext.TenantMemberships.AsNoTracking().SingleAsync(value =>
|
|
value.TenantId == grant.TenantId && value.UserId == grant.UserId &&
|
|
value.Status == MembershipStatus.Active, token);
|
|
var tokens = await services.GetRequiredService<IAuthSessionStore>().IssueAsync(
|
|
new AuthSessionIssueRequest(
|
|
user.Id, user.Phone, user.Email, user.SecurityStamp ?? string.Empty,
|
|
AuthRealm.Tenant, tenant.Id, "owner_activation", ipAddress, userAgent), token);
|
|
dbContext.AuthLoginEvents.Add(new AuthLoginEvent
|
|
{
|
|
TenantId = tenant.Id,
|
|
UserId = user.Id,
|
|
Provider = "owner_activation",
|
|
Identifier = user.Email ?? user.Phone,
|
|
Result = AuthLoginResult.Success,
|
|
IpAddress = ipAddress,
|
|
UserAgent = userAgent
|
|
});
|
|
authentication = new AuthenticationResult(
|
|
AuthenticationStatus.Authenticated,
|
|
new AuthenticatedUser(
|
|
user.Id, user.Phone, user.Email, user.Name, AuthRealm.Tenant,
|
|
new TenantMembershipSummary(tenant.Id, tenant.Name, membership.Role, membership.Status),
|
|
tokens));
|
|
}
|
|
|
|
await dbContext.SaveChangesAsync(token);
|
|
return authentication;
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
private static OwnerActivationException Error(string message, string code)
|
|
{
|
|
return new OwnerActivationException(message, code);
|
|
}
|
|
} |