Concurrent lisans sistemi SessionId düzenlemesi
This commit is contained in:
parent
cb6b38f141
commit
d9f51b1c71
14 changed files with 354 additions and 55 deletions
|
|
@ -28,5 +28,6 @@ public class CreateUpdateTenantInput
|
|||
public string Email { get; set; }
|
||||
public string Website { get; set; }
|
||||
public string MenuGroup { get; set; }
|
||||
public int MaxConcurrentUsers { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
|||
entity.SetEmail(input.Data.Email);
|
||||
entity.SetWebsite(input.Data.Website);
|
||||
entity.SetMenuGroup(input.Data.MenuGroup);
|
||||
entity.SetMaxConcurrentUsers(input.Data.MaxConcurrentUsers);
|
||||
|
||||
await tenantRepository.UpdateAsync(entity, autoSave: true);
|
||||
}
|
||||
|
|
@ -289,6 +290,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic
|
|||
entity.SetEmail(input.Data.Email);
|
||||
entity.SetWebsite(input.Data.Website);
|
||||
entity.SetMenuGroup(input.Data.MenuGroup);
|
||||
entity.SetMaxConcurrentUsers(input.Data.MaxConcurrentUsers);
|
||||
|
||||
await tenantRepository.UpdateAsync(entity, autoSave: true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5090,6 +5090,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
Text = "App.Platform.HangfireLogin",
|
||||
AuthName=listFormName,
|
||||
Url= swaggerRootUrl + "/Account/Login",
|
||||
Icon = "login",
|
||||
IsVisible = true,
|
||||
},
|
||||
new() {
|
||||
|
|
@ -5098,6 +5099,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
Text = "App.Platform.HangfireOpen",
|
||||
AuthName=listFormName,
|
||||
Url= swaggerRootUrl + "/hangfire",
|
||||
Icon = "runner",
|
||||
IsVisible = true,
|
||||
},
|
||||
new() {
|
||||
|
|
@ -5106,6 +5108,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
Text = "App.Platform.HangfireRefresh",
|
||||
AuthName=listFormName,
|
||||
OnClick="UiEvalService.ApiGenerateBackgroundWorkers();",
|
||||
Icon = "refresh",
|
||||
IsVisible = true,
|
||||
},
|
||||
}),
|
||||
|
|
@ -6137,6 +6140,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
Text = "Swagger",
|
||||
AuthName = listFormName,
|
||||
Url= swaggerRootUrl + "/swagger/index.html",
|
||||
Icon = "doc",
|
||||
IsVisible = true
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -19,5 +19,11 @@ public static class PlatformClaimTypes
|
|||
public static KeyValuePair<string, string> IsVerified { get; set; }
|
||||
= new KeyValuePair<string, string>(PlatformConsts.AbpIdentity.User.IsVerified, "platform.is_verified");
|
||||
|
||||
/// <summary>
|
||||
/// Default: "platform.session_id".
|
||||
/// </summary>
|
||||
public static KeyValuePair<string, string> SessionId { get; set; }
|
||||
= new KeyValuePair<string, string>("platform.session_id", "platform.session_id");
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Sozsoft.Platform.Identity;
|
||||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
|
||||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Entities;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.Users;
|
||||
|
||||
namespace Sozsoft.Platform.Extensions;
|
||||
|
||||
|
|
@ -29,6 +33,20 @@ public class PlatformApplicationConfigurationContributor : IApplicationConfigura
|
|||
var customComponentRepository = context.ServiceProvider.GetRequiredService<IRepository<CustomComponent, Guid>>();
|
||||
var customComponents = await customComponentRepository.GetListAsync() ?? throw new EntityNotFoundException("RecordNotFound");
|
||||
context.ApplicationConfiguration.SetProperty("customComponents", customComponents);
|
||||
|
||||
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
|
||||
if (currentUser.Id.HasValue)
|
||||
{
|
||||
var sessionRepository = context.ServiceProvider.GetRequiredService<IIdentitySessionRepository>();
|
||||
var sessions = await sessionRepository.GetListAsync(userId: currentUser.Id.Value);
|
||||
var claimSessionId = currentUser.FindClaim(PlatformClaimTypes.SessionId.Key)?.Value;
|
||||
var sessionId = sessions.Exists(session => session.SessionId == claimSessionId)
|
||||
? claimSessionId
|
||||
: sessions.OrderByDescending(session => session.SignedIn).FirstOrDefault()?.SessionId;
|
||||
|
||||
context.ApplicationConfiguration.CurrentUser.SessionId = sessionId;
|
||||
context.ApplicationConfiguration.SetProperty("currentUserSessionId", sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Sozsoft.Platform.Identity;
|
||||
|
||||
public static class PlatformSessionClaimContext
|
||||
{
|
||||
private static readonly AsyncLocal<string> CurrentSessionId = new();
|
||||
|
||||
public static string SessionId => CurrentSessionId.Value;
|
||||
|
||||
public static IDisposable Change(string sessionId)
|
||||
{
|
||||
var previous = CurrentSessionId.Value;
|
||||
CurrentSessionId.Value = sessionId;
|
||||
return new DisposeAction(() => CurrentSessionId.Value = previous);
|
||||
}
|
||||
|
||||
private sealed class DisposeAction : IDisposable
|
||||
{
|
||||
private readonly Action action;
|
||||
|
||||
public DisposeAction(Action action)
|
||||
{
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using Volo.Abp.DependencyInjection;
|
|||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.TenantManagement;
|
||||
using Volo.Abp.Timing;
|
||||
using Volo.Abp.Uow;
|
||||
using Sozsoft.Platform.Extensions;
|
||||
|
||||
|
|
@ -27,26 +28,31 @@ public class PlatformSessionCleanupWorker : ISessionCleanupWorker, ITransientDep
|
|||
private readonly ICurrentTenant currentTenant;
|
||||
private readonly ILogger<PlatformSessionCleanupWorker> logger;
|
||||
private readonly IOptions<OpenIddictServerOptions> openIddictOptions;
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
private readonly IClock clock;
|
||||
|
||||
public PlatformSessionCleanupWorker(
|
||||
IIdentitySessionRepository sessionRepo,
|
||||
ITenantRepository tenantRepository,
|
||||
ICurrentTenant currentTenant,
|
||||
ILogger<PlatformSessionCleanupWorker> logger,
|
||||
IOptions<OpenIddictServerOptions> openIddictOptions)
|
||||
IOptions<OpenIddictServerOptions> openIddictOptions,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IClock clock)
|
||||
{
|
||||
this.sessionRepo = sessionRepo;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.currentTenant = currentTenant;
|
||||
this.logger = logger;
|
||||
this.openIddictOptions = openIddictOptions;
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
[UnitOfWork]
|
||||
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var refreshTokenLifetime = openIddictOptions.Value.RefreshTokenLifetime ?? TimeSpan.FromMinutes(90);
|
||||
var cutoff = DateTime.Now - refreshTokenLifetime - TimeSpan.FromMinutes(5);
|
||||
var cutoff = clock.Now - refreshTokenLifetime - TimeSpan.FromMinutes(5);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -62,6 +68,8 @@ public class PlatformSessionCleanupWorker : ISessionCleanupWorker, ITransientDep
|
|||
{
|
||||
using (currentTenant.Change(tenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
|
||||
var sessions = await sessionRepo.GetListAsync(cancellationToken: cancellationToken);
|
||||
var staleSessions = sessions
|
||||
.Where(s => (s.LastAccessed ?? s.SignedIn) < cutoff)
|
||||
|
|
@ -76,8 +84,10 @@ public class PlatformSessionCleanupWorker : ISessionCleanupWorker, ITransientDep
|
|||
tenantId,
|
||||
session.UserId,
|
||||
session.SignedIn,
|
||||
DateTime.UtcNow);
|
||||
clock.Now);
|
||||
}
|
||||
|
||||
await uow.CompleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ using Volo.Abp.DependencyInjection;
|
|||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.Security.Claims;
|
||||
using Volo.Abp.Uow;
|
||||
using System.Linq;
|
||||
using Volo.Abp;
|
||||
|
||||
namespace Sozsoft.Platform.Identity;
|
||||
|
||||
|
|
@ -23,15 +25,18 @@ public class PlatformSessionRevocationHandler :
|
|||
{
|
||||
private readonly IIdentitySessionRepository sessionRepo;
|
||||
private readonly ICurrentTenant currentTenant;
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
private readonly ILogger<PlatformSessionRevocationHandler> logger;
|
||||
|
||||
public PlatformSessionRevocationHandler(
|
||||
IIdentitySessionRepository sessionRepo,
|
||||
ICurrentTenant currentTenant,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<PlatformSessionRevocationHandler> logger)
|
||||
{
|
||||
this.sessionRepo = sessionRepo;
|
||||
this.currentTenant = currentTenant;
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
|
@ -45,14 +50,20 @@ public class PlatformSessionRevocationHandler :
|
|||
|
||||
// Token'dan tenant ID'yi al.
|
||||
var tenantId = ParseTenantId(context.GenericTokenPrincipal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId)?.Value);
|
||||
var sessionId = context.GenericTokenPrincipal?.Claims.FirstOrDefault(c => c.Type == PlatformClaimTypes.SessionId.Key)?.Value;
|
||||
|
||||
try
|
||||
{
|
||||
// Sadece token'ın ait olduğu tenant'taki oturumları sil.
|
||||
using (currentTenant.Change(tenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await sessionRepo.GetListAsync(userId: userGuid);
|
||||
foreach (var session in sessions)
|
||||
var sessionsToDelete = sessionId.IsNullOrWhiteSpace()
|
||||
? sessions
|
||||
: sessions.Where(session => session.SessionId == sessionId).ToList();
|
||||
|
||||
foreach (var session in sessionsToDelete)
|
||||
{
|
||||
await sessionRepo.DeleteAsync(session);
|
||||
|
||||
|
|
@ -63,6 +74,8 @@ public class PlatformSessionRevocationHandler :
|
|||
session.SignedIn,
|
||||
DateTime.UtcNow);
|
||||
}
|
||||
|
||||
await uow.CompleteAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.Security.Claims;
|
||||
using Volo.Abp.Uow;
|
||||
|
||||
namespace Sozsoft.Platform.Identity;
|
||||
|
||||
public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDependency
|
||||
{
|
||||
private readonly IIdentitySessionRepository sessionRepository;
|
||||
private readonly ICurrentTenant currentTenant;
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
|
||||
public PlatformSessionValidationMiddleware(
|
||||
IIdentitySessionRepository sessionRepository,
|
||||
ICurrentTenant currentTenant,
|
||||
IUnitOfWorkManager unitOfWorkManager)
|
||||
{
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.currentTenant = currentTenant;
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var userIdValue = context.User.FindFirst(AbpClaimTypes.UserId)?.Value;
|
||||
var sessionId = context.User.FindFirst(PlatformClaimTypes.SessionId.Key)?.Value;
|
||||
|
||||
if (!Guid.TryParse(userIdValue, out var userId) || sessionId.IsNullOrWhiteSpace())
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value);
|
||||
using (currentTenant.Change(tenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await sessionRepository.GetListAsync(userId: userId);
|
||||
var isCurrentSession = sessions.Any(session => session.SessionId == sessionId);
|
||||
await uow.CompleteAsync();
|
||||
|
||||
if (!isCurrentSession)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
|
||||
private static Guid? ParseTenantId(string value)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ using Volo.Abp.MultiTenancy;
|
|||
using Volo.Abp.Settings;
|
||||
using Volo.Abp.TenantManagement;
|
||||
using Volo.Abp.Timing;
|
||||
using Volo.Abp.Uow;
|
||||
using IdentityUser = Volo.Abp.Identity.IdentityUser;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
|||
private readonly IdentityUserManager userManager;
|
||||
private readonly IIdentitySessionRepository identitySessionRepository;
|
||||
private readonly ICurrentTenant currentTenant;
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
|
||||
public PlatformSignInManager(
|
||||
IdentityUserManager userManager,
|
||||
|
|
@ -53,7 +55,8 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
|||
IRepository<WorkHour, Guid> repositoryWorkHour,
|
||||
ITenantRepository tenantRepository,
|
||||
IIdentitySessionRepository identitySessionRepository,
|
||||
ICurrentTenant currentTenant
|
||||
ICurrentTenant currentTenant,
|
||||
IUnitOfWorkManager unitOfWorkManager
|
||||
) : base(
|
||||
userManager,
|
||||
contextAccessor,
|
||||
|
|
@ -72,6 +75,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
|||
this.userManager = userManager;
|
||||
this.identitySessionRepository = identitySessionRepository;
|
||||
this.currentTenant = currentTenant;
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
}
|
||||
|
||||
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
|
||||
|
|
@ -296,6 +300,7 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
|||
// doğru tenant izolasyonu için güvenli bir şekilde değiştirilir.
|
||||
using (currentTenant.Change(user.TenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await identitySessionRepository.GetListAsync();
|
||||
|
||||
// Bu kullanıcı hariç diğer distinct aktif kullanıcı sayısını hesapla.
|
||||
|
|
@ -313,6 +318,8 @@ public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
|||
user.TenantId, maxConcurrentUsers.Value, otherActiveUserCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
await uow.CompleteAsync();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sozsoft.Platform.Extensions;
|
||||
|
|
@ -37,6 +38,8 @@ public class PlatformTokenController : TokenController
|
|||
private readonly ICaptchaManager captchaManager;
|
||||
private readonly IPlatformSignInManager platformSignInManager;
|
||||
private readonly IIdentitySessionRepository identitySessionRepository;
|
||||
private readonly IOpenIddictTokenManager openIddictTokenManager;
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
private IStringLocalizer LP;
|
||||
|
||||
public PlatformTokenController(
|
||||
|
|
@ -44,12 +47,16 @@ public class PlatformTokenController : TokenController
|
|||
ICaptchaManager captchaManager,
|
||||
IPlatformSignInManager platformSignInManager,
|
||||
IIdentitySessionRepository identitySessionRepository,
|
||||
IOpenIddictTokenManager openIddictTokenManager,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IStringLocalizer<PlatformResource> LP) : base()
|
||||
{
|
||||
this.emailSender = emailSender;
|
||||
this.captchaManager = captchaManager;
|
||||
this.platformSignInManager = platformSignInManager;
|
||||
this.identitySessionRepository = identitySessionRepository;
|
||||
this.openIddictTokenManager = openIddictTokenManager;
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
this.LP = LP;
|
||||
}
|
||||
|
||||
|
|
@ -235,43 +242,32 @@ Your login code: {twoFactorToken}";
|
|||
|
||||
protected override async Task<IActionResult> SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user)
|
||||
{
|
||||
var result = await base.SetSuccessResultAsync(request, user);
|
||||
await ManageIdentitySessionAsync(user, request);
|
||||
return result;
|
||||
var sessionId = request.IsRefreshTokenGrantType()
|
||||
? PlatformSessionClaimContext.SessionId
|
||||
: CreateSessionId();
|
||||
|
||||
using (PlatformSessionClaimContext.Change(sessionId))
|
||||
{
|
||||
var result = await base.SetSuccessResultAsync(request, user);
|
||||
await ManageIdentitySessionAsync(user, request, sessionId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ManageIdentitySessionAsync(IdentityUser user, OpenIddictRequest request)
|
||||
private async Task ManageIdentitySessionAsync(IdentityUser user, OpenIddictRequest request, string sessionId)
|
||||
{
|
||||
// Refresh grant session yönetimi HandleRefreshTokenAsync'da yapılır.
|
||||
if (request.IsRefreshTokenGrantType()) return;
|
||||
|
||||
try
|
||||
{
|
||||
using (CurrentTenant.Change(user.TenantId))
|
||||
{
|
||||
var existingSessions = await identitySessionRepository.GetListAsync(userId: user.Id);
|
||||
if (existingSessions.Any())
|
||||
{
|
||||
foreach (var session in existingSessions)
|
||||
{
|
||||
await identitySessionRepository.DeleteAsync(session);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!await platformSignInManager.CheckConcurrentLimitAsync(user))
|
||||
{
|
||||
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||
}
|
||||
}
|
||||
|
||||
await InsertSessionAsync(user, request.ClientId);
|
||||
}
|
||||
await ReplaceUserSessionAsync(user, request.ClientId, sessionId, checkConcurrentLimit: true);
|
||||
}
|
||||
catch (UserFriendlyException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to manage AbpSessions. UserId: {UserId}", user.Id);
|
||||
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,56 +275,185 @@ Your login code: {twoFactorToken}";
|
|||
protected override async Task<IActionResult> HandleRefreshTokenAsync(OpenIddictRequest request)
|
||||
{
|
||||
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
var tenantId = ParseTenantId(info.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId)?.Value);
|
||||
var userId = info.Principal?.GetClaim(OpenIddictConstants.Claims.Subject);
|
||||
var tenantId = await ResolveRefreshTenantIdAsync(info);
|
||||
var userGuid = await ResolveRefreshUserIdAsync(request, info, tenantId);
|
||||
var sessionId = await ResolveRefreshSessionIdAsync(request, info, tenantId);
|
||||
|
||||
IdentityUser refreshUser = null;
|
||||
|
||||
using (CurrentTenant.Change(tenantId))
|
||||
{
|
||||
if (!userId.IsNullOrWhiteSpace() && Guid.TryParse(userId, out var userGuid))
|
||||
if (userGuid.HasValue)
|
||||
{
|
||||
refreshUser = await UserManager.FindByIdAsync(userId);
|
||||
if (refreshUser != null)
|
||||
if (!await IsCurrentSessionAsync(tenantId, userGuid.Value, sessionId))
|
||||
{
|
||||
// Concurrent login limiti refresh token için de geçerlidir; yeni oturum açılmadan önce eskiler temizlenir.
|
||||
if (!await platformSignInManager.CheckConcurrentLimitAsync(refreshUser))
|
||||
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||
|
||||
var existingSessions = await identitySessionRepository.GetListAsync(userId: userGuid);
|
||||
foreach (var session in existingSessions)
|
||||
{
|
||||
await identitySessionRepository.DeleteAsync(session);
|
||||
}
|
||||
return InvalidRefreshSessionResult();
|
||||
}
|
||||
|
||||
refreshUser = await UserManager.FindByIdAsync(userGuid.Value.ToString());
|
||||
}
|
||||
|
||||
var result = await base.HandleRefreshTokenAsync(request);
|
||||
IActionResult result;
|
||||
using (PlatformSessionClaimContext.Change(sessionId.IsNullOrWhiteSpace() ? CreateSessionId() : sessionId))
|
||||
{
|
||||
result = await base.HandleRefreshTokenAsync(request);
|
||||
}
|
||||
|
||||
if (result is Microsoft.AspNetCore.Mvc.SignInResult && refreshUser != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InsertSessionAsync(refreshUser, request.ClientId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to create IdentitySession after refresh token. UserId: {UserId}", refreshUser.Id);
|
||||
}
|
||||
await ReplaceUserSessionAsync(
|
||||
refreshUser,
|
||||
request.ClientId,
|
||||
sessionId.IsNullOrWhiteSpace() ? CreateSessionId() : sessionId,
|
||||
checkConcurrentLimit: false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InsertSessionAsync(IdentityUser user, string clientId)
|
||||
private async Task<Guid?> ResolveRefreshTenantIdAsync(AuthenticateResult info)
|
||||
{
|
||||
var tenantId = ParseTenantId(info.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId)?.Value);
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
var tenant = await TenantConfigurationProvider.GetAsync(saveResolveResult: false);
|
||||
return tenant?.Id;
|
||||
}
|
||||
|
||||
private async Task<Guid?> ResolveRefreshUserIdAsync(OpenIddictRequest request, AuthenticateResult info, Guid? tenantId)
|
||||
{
|
||||
var userId = info.Principal?.GetClaim(OpenIddictConstants.Claims.Subject);
|
||||
var userGuid = ParseUserId(userId);
|
||||
if (userGuid.HasValue)
|
||||
{
|
||||
return userGuid;
|
||||
}
|
||||
|
||||
if (request.RefreshToken.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (CurrentTenant.Change(tenantId))
|
||||
{
|
||||
var token = await openIddictTokenManager.FindByReferenceIdAsync(request.RefreshToken);
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var subject = await openIddictTokenManager.GetSubjectAsync(token);
|
||||
return ParseUserId(subject);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveRefreshSessionIdAsync(OpenIddictRequest request, AuthenticateResult info, Guid? tenantId)
|
||||
{
|
||||
var sessionId = GetSessionId(info.Principal);
|
||||
if (!sessionId.IsNullOrWhiteSpace())
|
||||
{
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
if (request.RefreshToken.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (CurrentTenant.Change(tenantId))
|
||||
{
|
||||
var token = await openIddictTokenManager.FindByReferenceIdAsync(request.RefreshToken);
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var properties = await openIddictTokenManager.GetPropertiesAsync(token);
|
||||
return properties.TryGetValue(PlatformClaimTypes.SessionId.Key, out var propertySessionId)
|
||||
? propertySessionId.GetString()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateSessionId()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
private static string GetSessionId(ClaimsPrincipal principal)
|
||||
{
|
||||
return principal?.FindFirst(PlatformClaimTypes.SessionId.Key)?.Value;
|
||||
}
|
||||
|
||||
private async Task<bool> IsCurrentSessionAsync(Guid? tenantId, Guid userId, string sessionId)
|
||||
{
|
||||
if (sessionId.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (CurrentTenant.Change(tenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await identitySessionRepository.GetListAsync(userId: userId);
|
||||
var isCurrent = sessions.Any(session => session.SessionId == sessionId);
|
||||
await uow.CompleteAsync();
|
||||
return isCurrent;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReplaceUserSessionAsync(
|
||||
IdentityUser user,
|
||||
string clientId,
|
||||
string sessionId,
|
||||
bool checkConcurrentLimit)
|
||||
{
|
||||
using (CurrentTenant.Change(user.TenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var existingSessions = await identitySessionRepository.GetListAsync(userId: user.Id);
|
||||
|
||||
if (checkConcurrentLimit && !existingSessions.Any())
|
||||
{
|
||||
if (!await platformSignInManager.CheckConcurrentLimitAsync(user))
|
||||
{
|
||||
throw new UserFriendlyException(PlatformConsts.UserCannotSignInErrors.LoginNotAllowed_ConcurrentUserLimit);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var session in existingSessions)
|
||||
{
|
||||
await identitySessionRepository.DeleteAsync(session);
|
||||
}
|
||||
|
||||
await InsertSessionAsync(user, clientId, sessionId);
|
||||
await uow.CompleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult InvalidRefreshSessionResult()
|
||||
{
|
||||
var properties = new AuthenticationProperties(new Dictionary<string, string>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token session is no longer active."
|
||||
});
|
||||
|
||||
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
private async Task InsertSessionAsync(IdentityUser user, string clientId, string sessionId)
|
||||
{
|
||||
var userAgent = HttpContext?.Request?.Headers["User-Agent"].ToString();
|
||||
var deviceInfo = userAgent?.Length > 64 ? userAgent[..64] : userAgent;
|
||||
|
||||
var session = new IdentitySession(
|
||||
GuidGenerator.Create(),
|
||||
Guid.NewGuid().ToString("N"),
|
||||
sessionId,
|
||||
"Web",
|
||||
deviceInfo,
|
||||
user.Id,
|
||||
|
|
@ -343,6 +468,9 @@ Your login code: {twoFactorToken}";
|
|||
|
||||
private static Guid? ParseTenantId(string value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
||||
|
||||
private static Guid? ParseUserId(string value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ public class PlatformUserClaimsPrincipalFactory : AbpUserClaimsPrincipalFactory,
|
|||
|
||||
identity.AddClaim(new Claim(PlatformConsts.AbpIdentity.User.IsVerified, user.GetIsVerified().ToString()));
|
||||
|
||||
if (!PlatformSessionClaimContext.SessionId.IsNullOrWhiteSpace())
|
||||
{
|
||||
identity.AddIfNotContains(
|
||||
new Claim(PlatformClaimTypes.SessionId.Key, PlatformSessionClaimContext.SessionId));
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
}
|
||||
|
||||
app.UseUnitOfWork();
|
||||
app.UseMiddleware<PlatformSessionValidationMiddleware>();
|
||||
app.UseDynamicClaims();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export interface CurrentUserDto {
|
|||
phoneNumber?: string
|
||||
phoneNumberVerified: boolean
|
||||
roles: string[]
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
export interface DateTimeFormatDto {
|
||||
|
|
|
|||
Loading…
Reference in a new issue